Roulette Merit SkipSpin

Skip the roulette animation by retrying the most recent bet without spinning the wheel — grind the Spinner merit fast!

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Roulette Merit SkipSpin
// @namespace    http://tampermonkey.net/
// @version      3.2.1
// @description  Skip the roulette animation by retrying the most recent bet without spinning the wheel — grind the Spinner merit fast!
// @author       Ashbrak
// @match        https://www.torn.com/page.php?sid=roulette
// @grant        none
// @license      MIT
// ==/UserScript==
//
// MIT License
// Copyright (c) 2026 Ashbrak
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

(function () {
    'use strict';

    let lastBetUrl = null;

    // ── Styles ────────────────────────────────────────────────────────────────
    const styles = `
        <style>
            #ashbrakWrapper {
                position: relative;
                z-index: 999999;
                display: flex;
                flex-direction: column;
                align-items: center;
                gap: 6px;
                padding: 10px;
                margin: 12px auto;
                max-width: 400px;
            }
            #ashbrakInfo {
                font-size: 14px;
                font-weight: bold;
                text-align: center;
                min-height: 20px;
            }
            #ashbrakInfo.green { color: #66bb6a; }
            #ashbrakInfo.red   { color: #ef5350; }
            #ashbrakRetry {
                background: linear-gradient(45deg, #4caf50, #2e7d32);
                border: 2px solid #1b5e20;
                border-radius: 6px;
                color: #fff;
                cursor: pointer;
                font-size: 15px;
                font-weight: bold;
                padding: 12px 24px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.5);
                transition: transform 0.15s ease, box-shadow 0.15s ease;
                user-select: none;
                -webkit-user-select: none;
                touch-action: manipulation;
            }
            #ashbrakRetry:hover {
                transform: scale(1.06);
                box-shadow: 0 6px 16px rgba(0,0,0,0.6);
            }
            #ashbrakRetry:active {
                transform: scale(0.97);
            }
            #ashbrakRetry[disabled] {
                opacity: 0.4;
                cursor: not-allowed;
                background: #555;
                border-color: #333;
                transform: none !important;
            }
        </style>
    `;

    // ── Helpers ───────────────────────────────────────────────────────────────
    function toNumberFormat(n) {
        return Number(n).toLocaleString('en-US');
    }

    function displayInfo(msg, color) {
        const el = document.getElementById('ashbrakInfo');
        if (!el) return;
        el.textContent = msg;
        el.className = color || '';
    }

    function handleBetResponse(data) {
        if (!data || typeof data !== 'object') return;

        if (data.totalAmount !== undefined) {
            for (const sel of ['#st_money_val', '[id*="money"]', '[class*="moneyVal"]']) {
                const el = document.querySelector(sel);
                if (el) { el.textContent = '$' + toNumberFormat(data.totalAmount); break; }
            }
        }
        if (data.tokens !== undefined) {
            for (const sel of ['#st_tokens_val', '[id*="token"]', '[class*="tokenVal"]']) {
                const el = document.querySelector(sel);
                if (el) { el.textContent = data.tokens; break; }
            }
        }

        const num   = data.number ?? '?';
        const won   = data.won ?? null;
        const title = won ? `Won $${toNumberFormat(won)}!` : 'Lost...';
        displayInfo(`${title} Landed: ${num}`, won ? 'green' : 'red');
    }

    // ── Intercept GET requests ────────────────────────────────────────────────
    // Patch XHR at the prototype level — works on iOS WebKit where constructor
    // subclassing silently fails. We use addEventListener instead of wrapping
    // onreadystatechange so we never stomp on Torn's own handlers.
    const originalOpen = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function (method, url, ...rest) {
        if (typeof url === 'string' &&
            url.includes('sid=rouletteData') &&
            url.includes('step=processStakes')) {

            lastBetUrl = url;

            const btn = document.getElementById('ashbrakRetry');
            if (btn) btn.removeAttribute('disabled');

            this.addEventListener('load', function () {
                if (this.status === 200) {
                    try { handleBetResponse(JSON.parse(this.responseText)); } catch (_) {}
                }
            });
        }

        return originalOpen.apply(this, [method, url, ...rest]);
    };

    // Also cover fetch-based GETs
    const originalFetch = window.fetch.bind(window);
    window.fetch = async function (input, init = {}) {
        const url = typeof input === 'string' ? input : (input?.url ?? '');

        if (url.includes('sid=rouletteData') && url.includes('step=processStakes')) {
            lastBetUrl = url;
            const btn = document.getElementById('ashbrakRetry');
            if (btn) btn.removeAttribute('disabled');

            const response = await originalFetch(input, init);
            response.clone().json().then(handleBetResponse).catch(() => {});
            return response;
        }

        return originalFetch(input, init);
    };

    // ── Retry logic ───────────────────────────────────────────────────────────
    function retryLastBet() {
        if (!lastBetUrl) {
            displayInfo('Place a bet first!', 'red');
            return;
        }

        displayInfo('Spinning...', '');

        const xhr = new XMLHttpRequest();
        xhr.open('GET', lastBetUrl, true);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.addEventListener('load', function () {
            if (xhr.status === 200) {
                try { handleBetResponse(JSON.parse(xhr.responseText)); }
                catch (_) { displayInfo('Error reading response', 'red'); }
            } else {
                displayInfo(`Failed (HTTP ${xhr.status})`, 'red');
            }
        });
        xhr.send();
    }

    // ── UI injection ──────────────────────────────────────────────────────────
    function injectUI() {
        if (document.getElementById('ashbrakRetry')) return;

        document.body.insertAdjacentHTML('beforeend', styles);

        const wrapper = document.createElement('div');
        wrapper.id = 'ashbrakWrapper';
        wrapper.innerHTML = `
            <div id="ashbrakInfo">Place a bet first</div>
            <button id="ashbrakRetry" disabled>⚡ Retry last bet [R]</button>
        `;

        const target =
            document.getElementById('rouletteContainer') ||
            document.querySelector('[class*="rouletteWrap"]') ||
            document.querySelector('[class*="casinoContent"]') ||
            document.querySelector('main');

        if (target && target.parentNode) {
            target.parentNode.insertBefore(wrapper, target.nextSibling);
        } else {
            document.body.appendChild(wrapper);
        }

        // Click handler
        document.getElementById('ashbrakRetry').addEventListener('click', function (e) {
            e.preventDefault();
            e.stopPropagation();
            retryLastBet();
        });

        // Keyboard shortcut: press R to retry (when not typing in an input)
        // e.repeat catches OS key-repeat from holding the key down — we ignore
        // those so each bet requires a fresh physical keypress.
        document.addEventListener('keydown', function (e) {
            if (e.repeat) return;
            if (e.key === 'r' || e.key === 'R') {
                const tag = document.activeElement?.tagName;
                if (tag === 'INPUT' || tag === 'TEXTAREA') return;
                if (!lastBetUrl) return;
                retryLastBet();
            }
        });
    }

    // ── Wait for page ─────────────────────────────────────────────────────────
    function waitForTable(attempts = 0) {
        if (attempts > 80) { injectUI(); return; }

        const found =
            document.getElementById('rouletteCanvas') ||
            document.getElementById('rouletteContainer') ||
            document.querySelector('[class*="roulette"]') ||
            document.querySelector('[class*="casino"]');

        if (found) { injectUI(); }
        else { setTimeout(() => waitForTable(attempts + 1), 300); }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => waitForTable());
    } else {
        waitForTable();
    }

})();