Skip the roulette animation by retrying the most recent bet without spinning the wheel — grind the Spinner merit fast!
// ==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();
}
})();