HuskerDaddy's martingale wager helper for Torn Russian Roulette.
// ==UserScript==
// @name HuskerDaddy's RR Script
// @namespace codex.torn.rr-helper
// @version 4.0.14
// @description HuskerDaddy's martingale wager helper for Torn Russian Roulette.
// @license MIT
// @match https://www.torn.com/page.php?sid=russianRoulette*
// @match https://www.torn.com/properties.php*
// @match https://www.torn.com/trade.php*
// @match https://www.torn.com/item.php*
// @match https://www.torn.com/factions.php*
// @grant none
// @run-at document-idle
// ==/UserScript==
(() => {
'use strict';
const STORAGE_KEY = 'torn-rr-helper-settings-v1';
const defaultState = { enabled: true, inactiveWalletAutofill: true, ghostTradeEnabled: true, quickJoinEnabled: true, hideMultiShotButtons: false, dailyExpenses: 0, forecastOpen: true, settingsOpen: false, turboMultiplier: 1, base: 100000, multiplier: 2, losses: 0, lastOutcome: null, minimized: false, position: null };
const load = () => {
try { return { ...defaultState, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') }; }
catch { return { ...defaultState }; }
};
let state = load();
const save = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
const money = amount => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(amount);
const turboMultiplier = () => state.turboMultiplier === 3 ? 3 : state.turboMultiplier === 2 ? 2 : 1;
const currentBet = () => Math.round(state.base * turboMultiplier() * Math.pow(state.multiplier, state.losses));
const nextBet = () => Math.round(currentBet() * state.multiplier);
const isVisible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
const isRussianRoulettePage = new URL(location.href).searchParams.get('sid') === 'russianRoulette';
const isVaultPage = location.pathname.endsWith('/properties.php') && location.hash.includes('tab=vault');
const isGhostTradePage = location.pathname.endsWith('/trade.php');
const isMedicalItemsPage = location.pathname.endsWith('/item.php') || location.pathname.endsWith('/factions.php');
const MEDICAL_STORE = 'torn-rr-medical-cooldown-v1';
if (!isRussianRoulettePage && !isVaultPage && !isGhostTradePage && !isMedicalItemsPage) return;
if (window.__tornRRHelperInstalled) return;
window.__tornRRHelperInstalled = true;
const autofillStyle = document.createElement('style');
autofillStyle.textContent = '.rr-helper-autofill-flash { border-color: #41b6ff !important; box-shadow: 0 0 0 2px rgba(65,182,255,.55) !important; transition: border-color .15s ease, box-shadow .15s ease; } .rr-helper-quick-confirm { animation: none !important; transition: none !important; opacity: 1 !important; visibility: visible !important; } #rr-helper-join-proxy { position: fixed; z-index: 1000000; min-width: 58px; min-height: 28px; border: 0; border-radius: 4px; color: #fff; background: #3d4958; font: 700 14px/1 system-ui, sans-serif; cursor: pointer; transform: translate(-50%, -50%) scale(1.5); touch-action: manipulation; } #rr-helper-join-proxy:active { background: #526277; } body.rr-helper-hide-multishot button.commitButton___AZapi[data-id="2"], body.rr-helper-hide-multishot button.commitButton___AZapi[data-id="3"], body.rr-helper-hide-multishot button[class*="commitButton"][data-id="2"], body.rr-helper-hide-multishot button[class*="commitButton"][data-id="3"] { display: none !important; }';
document.head.append(autofillStyle);
const autofillFlashTimers = new WeakMap();
const autofillFlashedValues = new Map();
function autofillFieldKey(input) {
const form = input.closest('form');
return [location.pathname, location.hash, input.getAttribute('aria-label'), form?.className, input.name, input.type].filter(Boolean).join('|');
}
function flashAutofilledInput(input, value) {
const key = autofillFieldKey(input);
const formattedValue = String(value);
if (autofillFlashedValues.get(key) === formattedValue) return;
autofillFlashedValues.set(key, formattedValue);
clearTimeout(autofillFlashTimers.get(input));
input.classList.remove('rr-helper-autofill-flash');
void input.offsetWidth;
input.classList.add('rr-helper-autofill-flash');
autofillFlashTimers.set(input, setTimeout(() => input.classList.remove('rr-helper-autofill-flash'), 650));
}
function setInputValue(input, value) {
const currentNumber = Number(String(input.value).replace(/[^\d.-]/g, ''));
const nextNumber = Number(String(value).replace(/[^\d.-]/g, ''));
if (Number.isFinite(currentNumber) && Number.isFinite(nextNumber) && currentNumber === nextNumber) return false;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
setter ? setter.call(input, value) : (input.value = value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
flashAutofilledInput(input, value);
return true;
}
if (isMedicalItemsPage) {
let lastMedicalEndTime = 0;
let lastMedicalSignature = '';
const saveMedicalCooldown = (seconds, signature) => {
if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 14 * 86400 || signature === lastMedicalSignature) return;
lastMedicalSignature = signature;
const endTime = Date.now() + seconds * 1000;
// A live countdown changes its displayed clock every second, but its end
// time stays effectively the same. Static confirmations are saved once.
if (Math.abs(endTime - lastMedicalEndTime) < 2000) return;
lastMedicalEndTime = endTime;
localStorage.setItem(MEDICAL_STORE, String(endTime));
};
const captureMedicalCooldown = () => {
const counters = document.querySelectorAll('.response-wrap .counter-wrap, .use-wrap .counter-wrap, [data-seconds-left], [data-time], .counter-wrap');
counters.forEach(counter => {
const message = counter.closest('p, form, .response-wrap, .use-wrap')?.textContent || '';
if (!/medical cooldown/i.test(message)) return;
const clock = String(counter.textContent || '').trim().match(/^(\d{1,3}):(\d{2}):(\d{2})$/);
const visibleSeconds = clock ? Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3]) : NaN;
const seconds = Number(counter.dataset.secondsLeft || visibleSeconds || counter.dataset.time || 0);
saveMedicalCooldown(seconds, `${message}|${counter.textContent}|${counter.dataset.secondsLeft || counter.dataset.time || ''}`);
});
// The confirmation wording differs between medical items and between
// desktop/PDA. Fall back to any displayed medical-cooldown clock.
const pageText = document.body?.innerText || document.body?.textContent || '';
const textMatch = pageText.match(/medical\s+cooldown[\s\S]{0,80}?(\d{1,3}):(\d{2}):(\d{2})/i);
if (!textMatch) return;
const seconds = Number(textMatch[1]) * 3600 + Number(textMatch[2]) * 60 + Number(textMatch[3]);
saveMedicalCooldown(seconds, textMatch[0]);
};
captureMedicalCooldown();
new MutationObserver(captureMedicalCooldown).observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true });
return;
}
if (isVaultPage) {
let vaultFillQueued = false;
let vaultRetryTimers = [];
const vaultToggleStyle = document.createElement('style');
vaultToggleStyle.textContent = `
#rr-vault-toggle { cursor: pointer; }
#rr-vault-toggle .p-icon { display: inline-flex; align-items: center; justify-content: center; color: #f3f3f3; background: #777; font-family: inherit; font-size: 11px; font-weight: 700; line-height: 1; text-align: center; }
`;
document.head.append(vaultToggleStyle);
const renderVaultToggle = () => {
const button = document.querySelector('#rr-vault-toggle');
if (!button) return;
button.classList.toggle('is-off', !state.enabled);
const pressed = String(state.enabled);
const title = `RR Helper: ${state.enabled ? 'On' : 'Off'}`;
if (button.getAttribute('aria-pressed') !== pressed) button.setAttribute('aria-pressed', pressed);
if (button.title !== title) button.title = title;
const icon = button.querySelector('.p-icon');
const description = button.querySelector('.desc');
if (icon && icon.textContent !== 'RR') icon.textContent = 'RR';
const label = `RR Tracker: ${state.enabled ? 'On' : 'Off'}`;
if (description && description.textContent !== label) description.textContent = label;
};
const installVaultToggle = () => {
if (document.querySelector('#rr-vault-toggle')) { renderVaultToggle(); return; }
const button = document.querySelector('li.kick-prop');
if (!button) return;
button.id = 'rr-vault-toggle';
button.setAttribute('role', 'button');
button.dataset.rrTrackerBound = 'true';
button.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
state.enabled = !state.enabled;
save();
renderVaultToggle();
scheduleVaultFill();
});
renderVaultToggle();
};
const fillVaultAmount = () => {
const depositInput = [...document.querySelectorAll('form.deposit-box input.input-money[type="text"][data-deposit]')].find(isVisible);
const displayedWallet = Number(String(document.querySelector('#vault-dvalue')?.textContent || '').replace(/[^\d.-]/g, ''));
const depositedWallet = Number(String(depositInput?.dataset.deposit || '').replace(/[^\d.-]/g, ''));
const walletAmounts = [displayedWallet, depositedWallet].filter(amount => Number.isFinite(amount) && amount >= 0);
const walletAmount = walletAmounts.length ? Math.max(...walletAmounts) : NaN;
const filledDeposit = !!(depositInput && Number.isFinite(walletAmount));
if (filledDeposit) setInputValue(depositInput, String(walletAmount));
// Depositing all on-hand cash is safety-related and stays enabled even
// when bet autofill is paused.
if (!state.enabled) return filledDeposit;
const withdrawalInput = [...document.querySelectorAll('input.input-money[type="text"][data-money]')]
.find(input => isVisible(input) && !input.closest('form.deposit-box'));
if (withdrawalInput) setInputValue(withdrawalInput, String(currentBet()));
return filledDeposit || !!withdrawalInput;
};
const scheduleVaultFill = () => {
if (vaultFillQueued) return;
vaultFillQueued = true;
setTimeout(() => { vaultFillQueued = false; fillVaultAmount(); }, 50);
};
const retryVaultFill = () => {
vaultRetryTimers.forEach(clearTimeout);
vaultRetryTimers = [0, 250, 750].map(delay => setTimeout(() => {
state = load();
fillVaultAmount();
installVaultToggle();
}, delay));
};
retryVaultFill();
new MutationObserver(() => { scheduleVaultFill(); installVaultToggle(); }).observe(document.documentElement, { childList: true, subtree: true, characterData: true });
window.addEventListener('focus', retryVaultFill);
document.addEventListener('visibilitychange', () => { if (!document.hidden) retryVaultFill(); });
window.addEventListener('storage', event => {
if (event.key !== STORAGE_KEY) return;
state = load();
renderVaultToggle();
scheduleVaultFill();
});
return;
}
if (isGhostTradePage) {
let ghostTradeId = null;
let ghostTradeBalance = null;
let ghostTradeOutcome = null;
let ghostTradeTarget = null;
let ghostTradeMode = null;
let ghostTradeBet = null;
let ghostTradeFillQueued = false;
const isAddMoneyRoute = () => location.hash.includes('step=addmoney');
const currentTradeId = () => new URLSearchParams(location.hash.slice(1)).get('ID') || location.hash;
const findGhostTradeInput = () => [...document.querySelectorAll('input.user-id.input-money[data-money]')].find(isVisible);
const findAvailableCash = () => {
const amount = [...document.querySelectorAll('p .money-value')].find(element => element.closest('p')?.textContent.includes('You have'));
return amount ? Number(String(amount.textContent).replace(/[^\d.-]/g, '')) : null;
};
const findWalletCash = () => {
const wallet = document.querySelector('#user-money[data-money]');
const amount = Number(String(wallet?.dataset.money || '').replace(/[^\d.-]/g, ''));
return Number.isFinite(amount) ? amount : null;
};
const fillGhostTradeAmount = () => {
state = load();
if (!state.enabled || !state.ghostTradeEnabled || !isAddMoneyRoute()) return false;
const input = findGhostTradeInput();
if (!input) return false;
const tradeId = currentTradeId();
const reportedBalance = Number(String(input.dataset.money || '').replace(/[^\d.-]/g, ''));
if (ghostTradeMode === 'deposit' && Number.isFinite(reportedBalance) && reportedBalance === ghostTradeTarget) {
ghostTradeBalance = reportedBalance;
ghostTradeTarget = ghostTradeBalance;
ghostTradeMode = 'protected';
ghostTradeBet = currentBet();
state.lastOutcome = 'ready';
ghostTradeOutcome = state.lastOutcome;
save();
}
if (tradeId !== ghostTradeId || state.lastOutcome !== ghostTradeOutcome || (ghostTradeMode !== 'protected' && currentBet() !== ghostTradeBet) || !Number.isFinite(ghostTradeTarget)) {
ghostTradeId = tradeId;
ghostTradeBalance = reportedBalance;
ghostTradeOutcome = state.lastOutcome;
if (!Number.isFinite(ghostTradeBalance)) return false;
const availableCash = findAvailableCash();
// Torn can leave the "You have" line stale while this screen stays open.
// The header wallet is safe to use only immediately after a detected win;
// never use it when calculating the next withdrawal.
const walletCash = state.lastOutcome === 'win' ? findWalletCash() : null;
const depositCash = state.lastOutcome === 'win'
? Math.max(Number.isFinite(availableCash) ? availableCash : 0, Number.isFinite(walletCash) ? walletCash : 0)
: availableCash;
if (state.lastOutcome === null && !Number.isFinite(depositCash)) return false;
const shouldDeposit = Number.isFinite(depositCash) && depositCash > 0 && (state.lastOutcome === 'win' || state.lastOutcome === null);
if (shouldDeposit) {
ghostTradeTarget = ghostTradeBalance + depositCash;
ghostTradeMode = 'deposit';
} else {
ghostTradeTarget = Math.max(0, ghostTradeBalance - currentBet());
ghostTradeMode = 'withdraw';
}
ghostTradeBet = currentBet();
}
if (!Number.isFinite(ghostTradeTarget)) return false;
setInputValue(input, String(ghostTradeTarget));
return true;
};
const scheduleGhostTradeFill = () => {
if (ghostTradeFillQueued) return;
ghostTradeFillQueued = true;
setTimeout(() => { ghostTradeFillQueued = false; fillGhostTradeAmount(); }, 50);
};
fillGhostTradeAmount();
new MutationObserver(scheduleGhostTradeFill).observe(document.documentElement, { childList: true, subtree: true });
setInterval(() => { state = load(); fillGhostTradeAmount(); }, 300);
window.addEventListener('hashchange', () => {
if (ghostTradeMode === 'deposit' && location.hash.includes('step=view')) {
state.lastOutcome = 'ready';
save();
}
ghostTradeId = null;
ghostTradeBalance = null;
ghostTradeOutcome = null;
ghostTradeTarget = null;
ghostTradeMode = null;
ghostTradeBet = null;
scheduleGhostTradeFill();
});
window.addEventListener('storage', event => {
if (event.key !== STORAGE_KEY) return;
state = load();
scheduleGhostTradeFill();
});
return;
}
function findWagerInput() {
const selectors = [
'input[aria-label="Money value"]',
'input[name*="bet" i]',
'input[id*="bet" i]',
'input[name*="wager" i]',
'input[id*="wager" i]',
'input[placeholder*="amount" i]',
'input[type="number"]'
];
const candidates = [...document.querySelectorAll(selectors.join(','))]
.filter(input => !input.closest('#rr-helper-root') && !input.disabled && !input.readOnly && isVisible(input));
return candidates[0] || null;
}
function currentWalletCash() {
const wallet = document.querySelector('#user-money[data-money]');
const storedAmount = Number(String(wallet?.dataset.money || '').replace(/[^\d.-]/g, ''));
if (Number.isFinite(storedAmount)) return storedAmount;
const visibleWallet = document.querySelector('#user-money');
const visibleAmount = Number(String(visibleWallet?.textContent || '').replace(/[^\d.-]/g, ''));
return Number.isFinite(visibleAmount) ? visibleAmount : null;
}
function fillWager() {
// The Vault toggle can change this setting in another tab, so always use
// the current saved state before choosing wallet vs. martingale amount.
state = load();
if (!state.enabled && !state.inactiveWalletAutofill) return false;
const input = findWagerInput();
if (!input) return false;
const amount = state.enabled ? currentBet() : currentWalletCash();
if (!Number.isFinite(amount) || amount < 0) return false;
const value = String(amount);
setInputValue(input, value);
return true;
}
let wagerObserver = null;
function fillWagerWhenAvailable() {
state = load();
if (!state.enabled && !state.inactiveWalletAutofill) { wagerObserver?.disconnect(); wagerObserver = null; return; }
if (fillWager()) { wagerObserver?.disconnect(); wagerObserver = null; return; }
if (wagerObserver) return;
wagerObserver = new MutationObserver(() => {
if (fillWager()) { wagerObserver.disconnect(); wagerObserver = null; }
});
wagerObserver.observe(document.documentElement, { childList: true, subtree: true });
}
function retryWagerFill() {
[0, 180, 500].forEach(delay => setTimeout(() => {
state = load();
fillWagerWhenAvailable();
}, delay));
}
const root = document.createElement('div');
root.id = 'rr-helper-root';
root.innerHTML = `
<style>
#rr-helper-root { position: fixed; z-index: 2147483647; right: 18px; bottom: 18px; color: #e8edf6; font: 14px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
#rr-helper-root * { box-sizing: border-box; }
#rr-helper-root .rr-card { width: 310px; overflow: hidden; border: 1px solid rgba(255,255,255,.16); border-radius: 12px; background: #1b2029; box-shadow: 0 12px 38px rgba(0,0,0,.38); }
#rr-helper-root .rr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,.11); cursor: grab; touch-action: none; }
#rr-helper-root .rr-head:active { cursor: grabbing; }
#rr-helper-root .rr-brand { display: inline-flex; align-items: center; gap: 5px; min-width: 0; white-space: nowrap; }
#rr-helper-root .rr-logo { display: block; width: 42px; height: 28px; object-fit: contain; }
#rr-helper-root .rr-head > .rr-medical { font-size: 14px; white-space: nowrap; }
#rr-helper-root .rr-actions { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
#rr-helper-root button { border: 0; border-radius: 7px; padding: 7px 9px; color: inherit; background: #303846; font: inherit; cursor: pointer; }
#rr-helper-root button:hover { background: #3a4556; }
#rr-helper-root button[aria-pressed="true"] { color: #07170b; background: #7de095; }
#rr-helper-root button[aria-pressed="false"] { color: #f5d9d9; background: #69343b; }
#rr-helper-root .rr-minimize { min-width: 27px; padding-inline: 8px; }
#rr-helper-root .rr-head .rr-actions button { padding: 6px 8px; font-size: 12px; }
#rr-helper-root .rr-head .rr-actions .rr-minimize { min-width: 28px; padding-inline: 7px; }
#rr-helper-root .rr-body { padding: 14px; }
#rr-helper-root .rr-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
#rr-helper-root label { display: block; margin-bottom: 5px; color: #aeb8c8; font-size: 12px; }
#rr-helper-root input { width: 100%; min-width: 0; border: 1px solid #495568; border-radius: 7px; padding: 8px; color: #edf2fb; background: #11151c; font: inherit; }
#rr-helper-root input:disabled { opacity: .55; }
#rr-helper-root .rr-amounts { margin: 14px 0 0; border-top: 1px solid rgba(255,255,255,.11); }
#rr-helper-root .rr-line { display: flex; justify-content: space-between; gap: 10px; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.11); }
#rr-helper-root .rr-line span { color: #aeb8c8; }
#rr-helper-root .rr-profit-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-top: 10px; }
#rr-helper-root .rr-profit-row span { color: #aeb8c8; }
#rr-helper-root .rr-profit-input { width: 122px; border: 1px solid #495568; border-radius: 7px; padding: 7px 8px; color: #edf2fb; background: #11151c; font: inherit; text-align: right; }
#rr-helper-root .rr-expense-row { padding-top: 7px; }
#rr-helper-root .rr-forecast { margin-top: 14px; border-top: 1px solid rgba(255,255,255,.11); border-bottom: 1px solid rgba(255,255,255,.11); padding: 10px 0; }
#rr-helper-root .rr-forecast-toggle { width: 100%; padding: 0; color: #aeb8c8; background: transparent; text-align: left; font-size: 12px; }
#rr-helper-root .rr-forecast-toggle:hover { background: transparent; color: #edf2fb; }
#rr-helper-root .rr-forecast-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); overflow: hidden; border: 1px solid #394351; border-radius: 7px; background: #151a21; }
#rr-helper-root .rr-forecast-list[hidden] { display: none !important; }
#rr-helper-root .rr-forecast-item { display: flex; align-items: center; gap: 3px; min-width: 0; padding: 4px 5px; border-right: 1px solid rgba(255,255,255,.07); border-bottom: 1px solid rgba(255,255,255,.07); font-size: 9px; font-variant-numeric: tabular-nums; white-space: nowrap; }
#rr-helper-root .rr-forecast-item:nth-child(3n) { border-right: 0; }
#rr-helper-root .rr-forecast-item:last-child { border-bottom: 0; }
#rr-helper-root .rr-forecast-item:last-child:nth-child(3n + 1) { grid-column: 2; }
#rr-helper-root .rr-forecast-item span { color: #aeb8c8; }
#rr-helper-root .rr-forecast-item strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#rr-helper-root .rr-forecast-win { overflow: hidden; color: #7de095; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
#rr-helper-root .rr-forecast-win.is-loss { color: #f28b8b; }
#rr-helper-root .rr-settings-toggle { width: 100%; margin-top: 12px; text-align: left; }
#rr-helper-root .rr-settings-toggle[aria-expanded="true"] { background: #3a4556; }
#rr-helper-root .rr-settings { padding-top: 12px; }
#rr-helper-root .rr-turbo-row { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 10px; }
#rr-helper-root .rr-turbo.is-active { color: #201600; background: #f4cc54; }
#rr-helper-root .rr-wallet-autofill { display: flex; align-items: center; gap: 8px; margin-top: 11px; color: #aeb8c8; font-size: 12px; }
#rr-helper-root .rr-wallet-autofill input { width: auto; margin: 0; padding: 0; accent-color: #7de095; }
#rr-helper-root .rr-pill { display: none; align-items: center; gap: 5px; border: 1px solid rgba(255,255,255,.16); border-radius: 999px; padding: 5px; color: #edf2fb; background: #1b2029; box-shadow: 0 8px 26px rgba(0,0,0,.32); }
#rr-helper-root .rr-pill-main { border-radius: 999px; padding: 4px 7px; color: inherit; background: transparent; }
#rr-helper-root .rr-pill .rr-turbo { padding: 4px 6px; font-size: 12px; }
#rr-helper-root .rr-pill .rr-turbo.is-active { color: #201600; background: #f4cc54; }
#rr-helper-root .rr-medical { color: #aeb8c8; font-variant-numeric: tabular-nums; }
#rr-helper-root .rr-profit { color: #aeb8c8; font-variant-numeric: tabular-nums; }
@media (max-width: 340px) {
#rr-helper-root { right: 2px; }
#rr-helper-root .rr-card { width: min(310px, calc(100vw - 4px)); }
#rr-helper-root .rr-head { gap: 4px; padding: 8px; }
#rr-helper-root .rr-medical { font-size: 12px; }
#rr-helper-root .rr-actions { gap: 4px; }
#rr-helper-root .rr-actions button { padding: 5px 6px; font-size: 10px; }
#rr-helper-root .rr-actions .rr-minimize { min-width: 24px; padding-inline: 6px; }
}
#rr-helper-anchor { padding: 0; color: inherit; font: inherit; }
#rr-helper-anchor { display: inline-flex !important; flex-direction: row; align-items: center; gap: 5px; white-space: nowrap; }
#rr-helper-anchor button { border: 0; color: inherit; font: inherit; cursor: pointer; }
#rr-helper-anchor #rr-anchor-expand { display: inline-flex; align-items: center; gap: 5px; padding: 0; background: transparent; }
#rr-helper-anchor .rr-anchor-turbo { padding: 2px 4px; border-radius: 4px; background: rgba(255,255,255,.12); font-size: 12px; }
#rr-helper-anchor .rr-anchor-turbo.is-active { color: #201600; background: #f4cc54; }
#rr-helper-anchor #rr-helper-anchor-text { display: inline !important; white-space: nowrap; }
#rr-helper-anchor #rr-medical-compact { display: inline !important; white-space: nowrap; color: #8fd3ff; font-variant-numeric: tabular-nums; }
#rr-helper-anchor #rr-anchor-bets { display: inline-flex; align-items: center; gap: 5px; }
#rr-helper-anchor #rr-anchor-meta { display: inline-flex; align-items: center; gap: 5px; }
#rr-helper-root.is-minimized .rr-card { display: none; }
#rr-helper-root.is-minimized .rr-pill { display: inline-flex; }
#rr-helper-root.is-minimized.is-anchored .rr-pill { display: none; }
</style>
<div class="rr-pill"><button class="rr-pill-main" id="rr-expand" type="button" aria-label="Open Russian Roulette helper"><span class="rr-profit" id="rr-profit-fixed">$0</span> · <span class="rr-medical" id="rr-medical-fixed">--:--:--</span> · 🎲 <span id="rr-fixed-bets"></span></button><button class="rr-turbo" id="rr-turbo-2-pill" type="button" title="Start an x2 base-bet cycle">⚡</button><button class="rr-turbo" id="rr-turbo-3-pill" type="button" title="Start an x3 base-bet cycle">💣</button></div>
<section class="rr-card" aria-label="Russian Roulette bet settings">
<header class="rr-head"><span class="rr-brand"><img class="rr-logo" src="data:image/png;base64,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" alt="HD logo"></span><span class="rr-medical" id="rr-medical-full">--:--:--</span><span class="rr-actions"><button id="rr-power" type="button" aria-pressed="true">Active</button><button id="rr-reset" type="button" title="Reset chase to the base bet">Reset</button><button class="rr-minimize" id="rr-minimize" type="button" aria-label="Minimize helper">−</button></span></header>
<div class="rr-body">
<div class="rr-fields"><div><label for="rr-base">Base bet</label><input id="rr-base" type="number" min="1" step="1" inputmode="numeric"></div><div><label for="rr-multiplier">Multiplier</label><input id="rr-multiplier" type="number" min="1" step="0.1" inputmode="decimal"></div></div>
<div class="rr-turbo-row"><button class="rr-turbo" id="rr-turbo-2" type="button" title="Start an x2 base-bet cycle">⚡ x2 cycle</button><button class="rr-turbo" id="rr-turbo-3" type="button" title="Start an x3 base-bet cycle">💣 x3 cycle</button></div>
<div class="rr-amounts"><div class="rr-line"><span>Current bet</span><strong id="rr-current"></strong></div><div class="rr-line"><span>Next bet</span><strong id="rr-next"></strong></div></div>
<div class="rr-profit-row"><span>Daily P/L</span><input class="rr-profit-input" id="rr-profit-editor" type="text" inputmode="numeric" aria-label="Daily profit and loss"></div>
<div class="rr-profit-row rr-expense-row"><span>Daily expenses</span><input class="rr-profit-input" id="rr-expense-editor" type="text" inputmode="numeric" aria-label="Daily expenses"></div>
<div class="rr-forecast"><button class="rr-forecast-toggle" id="rr-forecast-toggle" type="button" aria-expanded="true">Next 25 games if each game loses ▾</button><div class="rr-forecast-list" id="rr-forecast-list"></div></div>
<button class="rr-settings-toggle" id="rr-settings-toggle" type="button" aria-expanded="false">Settings ▸</button>
<div class="rr-settings" id="rr-settings" hidden>
<label class="rr-wallet-autofill"><input id="rr-wallet-autofill" type="checkbox"> Wallet autofill while inactive</label>
<label class="rr-wallet-autofill"><input id="rr-ghost-trade-toggle" type="checkbox"> Enable Ghost Trade</label>
<label class="rr-wallet-autofill"><input id="rr-quick-join-toggle" type="checkbox"> Quick Join games</label>
<label class="rr-wallet-autofill"><input id="rr-multishot-toggle" type="checkbox"> Hide x2 / x3 buttons</label>
</div>
</div>
</section>`;
document.documentElement.append(root);
const ui = {
power: root.querySelector('#rr-power'), reset: root.querySelector('#rr-reset'), base: root.querySelector('#rr-base'), multiplier: root.querySelector('#rr-multiplier'), turbo2: root.querySelector('#rr-turbo-2'), turbo3: root.querySelector('#rr-turbo-3'), turbo2Pill: root.querySelector('#rr-turbo-2-pill'), turbo3Pill: root.querySelector('#rr-turbo-3-pill'),
current: root.querySelector('#rr-current'), next: root.querySelector('#rr-next'), pill: root.querySelector('.rr-pill'), fixedBets: root.querySelector('#rr-fixed-bets'),
medicalFull: root.querySelector('#rr-medical-full'), medicalFixed: root.querySelector('#rr-medical-fixed'), walletAutofill: root.querySelector('#rr-wallet-autofill'), ghostTradeToggle: root.querySelector('#rr-ghost-trade-toggle'), quickJoinToggle: root.querySelector('#rr-quick-join-toggle'), multiShotToggle: root.querySelector('#rr-multishot-toggle'),
profitEditor: root.querySelector('#rr-profit-editor'), expenseEditor: root.querySelector('#rr-expense-editor'), profitFixed: root.querySelector('#rr-profit-fixed'), forecastToggle: root.querySelector('#rr-forecast-toggle'), forecastList: root.querySelector('#rr-forecast-list'), settingsToggle: root.querySelector('#rr-settings-toggle'), settings: root.querySelector('#rr-settings')
};
const anchoredPill = document.createElement('span');
anchoredPill.id = 'rr-helper-anchor';
anchoredPill.innerHTML = '<button id="rr-anchor-expand" type="button" aria-label="Open Russian Roulette helper"><span id="rr-anchor-meta"><span id="rr-profit-compact">$0</span><span id="rr-medical-compact">--:--:--</span></span><span id="rr-anchor-bets"><span id="rr-helper-dice">🎲</span><span id="rr-helper-anchor-text"></span></span></button><button class="rr-anchor-turbo" id="rr-anchor-turbo-2" type="button" title="Start an x2 base-bet cycle">⚡</button><button class="rr-anchor-turbo" id="rr-anchor-turbo-3" type="button" title="Start an x3 base-bet cycle">💣</button>';
let anchorObserver = null;
let hiddenTitle = null;
let titleDisplay = '';
const rrNavStyle = document.createElement('style');
rrNavStyle.textContent = `
#rr-helper-nav { position: relative; display: inline-flex; align-items: center; }
#rr-helper-nav-toggle { border: 0; border-radius: 5px; padding: 4px 7px; color: inherit; background: transparent; font: 19px/1 sans-serif; cursor: pointer; }
#rr-helper-nav-toggle:hover, #rr-helper-nav-toggle[aria-expanded="true"] { background: rgba(255,255,255,.12); }
#rr-helper-nav-list { position: absolute; top: calc(100% + 6px); right: 0; z-index: 2147483646; display: none; min-width: 148px; overflow: hidden; border: 1px solid rgba(255,255,255,.2); border-radius: 7px; background: #242a33; box-shadow: 0 8px 22px rgba(0,0,0,.38); }
#rr-helper-nav.is-open #rr-helper-nav-list { display: block; }
#rr-helper-nav-list a { display: block; padding: 9px 12px; color: inherit; text-decoration: none; white-space: nowrap; }
#rr-helper-nav-list a:hover { background: rgba(255,255,255,.12); }
`;
document.head.append(rrNavStyle);
let rrNavFillQueued = false;
const menuLabel = label => label === 'Back to Casino' ? 'Back To Casino' : label;
function installRrNavigationMenu() {
const container = document.querySelector('[class*="linksContainer"]');
if (!container) return;
const links = [...container.querySelectorAll('a[role="button"]')];
if (links.length < 3) return;
const menuLinks = links.map(link => ({ href: link.href, label: link.textContent.trim() }));
const existingMenu = container.querySelector('#rr-helper-nav');
if (existingMenu) {
const items = [...existingMenu.querySelectorAll('#rr-helper-nav-list a')];
menuLinks.forEach(({ href, label }, index) => {
const item = items[index];
if (!item) return;
const displayLabel = menuLabel(label);
if (item.href !== href) item.href = href;
if (item.textContent !== displayLabel) item.textContent = displayLabel;
});
links.forEach(link => { link.style.display = 'none'; });
return;
}
links.forEach(link => { link.style.display = 'none'; });
const menu = document.createElement('div');
menu.id = 'rr-helper-nav';
menu.innerHTML = '<button id="rr-helper-nav-toggle" type="button" aria-label="Russian Roulette navigation" aria-expanded="false">☰</button><div id="rr-helper-nav-list"></div>';
const list = menu.querySelector('#rr-helper-nav-list');
menuLinks.forEach(({ href, label }) => {
const item = document.createElement('a');
item.href = href;
item.target = '_self';
item.rel = 'noreferrer';
item.textContent = menuLabel(label);
list.append(item);
});
const toggle = menu.querySelector('#rr-helper-nav-toggle');
const closeMenu = () => {
menu.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
};
toggle.addEventListener('click', event => {
event.stopPropagation();
const open = menu.classList.toggle('is-open');
toggle.setAttribute('aria-expanded', String(open));
});
list.addEventListener('click', closeMenu);
document.addEventListener('click', event => {
if (menu.contains(event.target)) return;
closeMenu();
});
container.append(menu);
}
function scheduleRrNavigationMenu() {
if (rrNavFillQueued) return;
rrNavFillQueued = true;
setTimeout(() => { rrNavFillQueued = false; installRrNavigationMenu(); }, 50);
}
function refreshRrNavigationAfterRouteChange() {
scheduleRrNavigationMenu();
// Torn finishes replacing the lobby header shortly after the URL changes.
setTimeout(installRrNavigationMenu, 250);
setTimeout(installRrNavigationMenu, 800);
}
function findRussianRouletteTitle() {
return document.querySelector('h4[class*="title"]');
}
function syncMinimizedAnchor() {
if (!state.minimized) {
if (hiddenTitle?.isConnected) hiddenTitle.style.display = titleDisplay;
hiddenTitle = null;
anchoredPill.remove();
root.classList.remove('is-anchored');
anchorObserver?.disconnect();
anchorObserver = null;
return;
}
const title = findRussianRouletteTitle();
if (!title) {
root.classList.remove('is-anchored');
if (!anchorObserver) {
anchorObserver = new MutationObserver(() => {
if (!findRussianRouletteTitle()) return;
anchorObserver.disconnect();
anchorObserver = null;
syncMinimizedAnchor();
});
anchorObserver.observe(document.documentElement, { childList: true, subtree: true });
}
return;
}
anchorObserver?.disconnect();
anchorObserver = null;
if (hiddenTitle !== title) {
if (hiddenTitle?.isConnected) hiddenTitle.style.display = titleDisplay;
hiddenTitle = title;
titleDisplay = title.style.display;
}
title.style.display = 'none';
anchoredPill.className = '';
if (anchoredPill.parentElement !== title.parentElement || anchoredPill.nextElementSibling !== title) title.before(anchoredPill);
root.classList.add('is-anchored');
}
let medicalEndTime = Number(localStorage.getItem(MEDICAL_STORE)) || 0;
function formatMedicalTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
return [hours, minutes, remainingSeconds].map(value => String(value).padStart(2, '0')).join(':');
}
function renderMedicalCooldown() {
const seconds = Math.max(0, Math.ceil((medicalEndTime - Date.now()) / 1000));
const label = seconds ? formatMedicalTime(seconds) : '';
ui.medicalFull.textContent = label;
ui.medicalFixed.textContent = label;
anchoredPill.querySelector('#rr-medical-compact').textContent = label;
if (!seconds && medicalEndTime) { medicalEndTime = 0; localStorage.removeItem(MEDICAL_STORE); }
}
const PROFIT_STORE = 'torn-rr-daily-profit-v1';
const profitDay = () => new Date().toISOString().slice(0, 10);
const loadProfit = () => {
try {
const saved = JSON.parse(localStorage.getItem(PROFIT_STORE) || '{}');
return saved.day === profitDay() && Number.isFinite(saved.profit) ? saved.profit : 0;
} catch { return 0; }
};
const savedProfitDay = () => {
try { return JSON.parse(localStorage.getItem(PROFIT_STORE) || '{}').day; }
catch { return null; }
};
let dailyProfit = loadProfit();
const netDailyProfit = () => dailyProfit - Math.max(0, Number(state.dailyExpenses) || 0);
function renderDailyProfit() {
if (profitDay() !== savedProfitDay()) dailyProfit = 0;
const netProfit = netDailyProfit();
const sign = netProfit > 0 ? '+' : netProfit < 0 ? '-' : '';
const value = `${sign}$${Math.abs(netProfit).toLocaleString('en-US')}`;
const color = netProfit > 0 ? '#7de095' : netProfit < 0 ? '#f28b8b' : '';
if (document.activeElement !== ui.profitEditor) ui.profitEditor.value = netProfit;
if (document.activeElement !== ui.expenseEditor) ui.expenseEditor.value = Math.max(0, Number(state.dailyExpenses) || 0);
ui.profitFixed.textContent = value;
anchoredPill.querySelector('#rr-profit-compact').textContent = value;
[ui.profitEditor, ui.profitFixed, anchoredPill.querySelector('#rr-profit-compact')].forEach(element => { element.style.color = color; });
}
function renderForecast() {
let wager = state.base;
let previousLosses = 0;
const compactMoney = amount => {
if (!Number.isFinite(amount)) return '—';
const suffixes = ['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc'];
const absolute = Math.abs(amount);
const tier = Math.min(Math.floor(Math.log10(absolute || 1) / 3), suffixes.length - 1);
const scaled = absolute / Math.pow(1000, tier);
const digits = scaled < 10 ? 2 : scaled < 100 ? 1 : 0;
return `$${scaled.toFixed(digits).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1')}${suffixes[tier]}`;
};
const rows = [];
for (let game = 1; game <= 25; game += 1) {
const label = Number.isFinite(wager) && wager >= 0 ? `$${money(wager)}` : '—';
const net = wager - previousLosses;
const netLabel = Number.isFinite(net) ? `${net >= 0 ? '+' : '-'}$${money(Math.abs(net))}` : '—';
const compactNet = Number.isFinite(net) ? `${net >= 0 ? '+' : '-'}${compactMoney(net)}` : '—';
rows.push(`<div class="rr-forecast-item" title="Game ${game}: Bet ${label}; net if won ${netLabel}"><span>${game}</span><strong>${compactMoney(wager)}</strong><span class="rr-forecast-win${net < 0 ? ' is-loss' : ''}">${compactNet}</span></div>`);
previousLosses += wager;
wager = Math.round(wager * state.multiplier);
}
ui.forecastList.innerHTML = rows.join('');
}
function recordDailyProfit(won) {
const pot = Number(String(document.querySelector('span[class^="count___"]')?.textContent || '').replace(/[^\d.-]/g, ''));
const amount = Number.isFinite(pot) && pot > 0 ? Math.round(pot / 2) : currentBet();
dailyProfit += won ? amount : -amount;
localStorage.setItem(PROFIT_STORE, JSON.stringify({ day: profitDay(), profit: dailyProfit }));
renderDailyProfit();
}
function renderTurboControls() {
const turbo = turboMultiplier();
[ui.turbo2, ui.turbo2Pill, anchoredPill.querySelector('#rr-anchor-turbo-2')].forEach(button => button.classList.toggle('is-active', turbo === 2));
[ui.turbo3, ui.turbo3Pill, anchoredPill.querySelector('#rr-anchor-turbo-3')].forEach(button => button.classList.toggle('is-active', turbo === 3));
}
function startTurbo(multiplier) {
state.turboMultiplier = multiplier;
state.losses = 0;
state.lastOutcome = 'ready';
save();
render();
fillWagerWhenAvailable();
}
function render() {
ui.power.textContent = state.enabled ? 'Active' : 'Inactive';
ui.power.setAttribute('aria-pressed', String(state.enabled));
if (document.activeElement !== ui.base) ui.base.value = state.base;
if (document.activeElement !== ui.multiplier) ui.multiplier.value = state.multiplier;
ui.walletAutofill.checked = Boolean(state.inactiveWalletAutofill);
ui.ghostTradeToggle.checked = Boolean(state.ghostTradeEnabled);
ui.quickJoinToggle.checked = Boolean(state.quickJoinEnabled);
ui.multiShotToggle.checked = Boolean(state.hideMultiShotButtons);
ui.forecastList.hidden = !state.forecastOpen;
ui.forecastToggle.setAttribute('aria-expanded', String(state.forecastOpen));
ui.forecastToggle.textContent = `Next 25 games if each game loses ${state.forecastOpen ? '▾' : '▸'}`;
ui.settings.hidden = !state.settingsOpen;
ui.settingsToggle.setAttribute('aria-expanded', String(state.settingsOpen));
ui.settingsToggle.textContent = `Settings ${state.settingsOpen ? '▾' : '▸'}`;
document.body.classList.toggle('rr-helper-hide-multishot', Boolean(state.hideMultiShotButtons));
ui.base.disabled = false;
ui.multiplier.disabled = false;
ui.current.textContent = `$${money(currentBet())}`;
ui.next.textContent = `$${money(nextBet())}`;
ui.fixedBets.textContent = `$${money(currentBet())} | $${money(nextBet())}`;
anchoredPill.querySelector('#rr-helper-anchor-text').textContent = `$${money(currentBet())} | $${money(nextBet())}`;
root.classList.toggle('is-minimized', state.minimized);
syncMinimizedAnchor();
renderDailyProfit();
renderForecast();
renderTurboControls();
}
function applyPosition() {
if (!state.position) return;
root.style.left = `${state.position.left}px`;
root.style.top = `${state.position.top}px`;
root.style.right = 'auto';
root.style.bottom = 'auto';
}
function handleResult(text) {
const message = text.toLowerCase();
if (message.includes('you take your winnings')) {
recordDailyProfit(true);
state.losses = 0;
state.turboMultiplier = 1;
state.lastOutcome = 'win';
save();
render();
if (state.enabled) fillWagerWhenAvailable();
} else if (message.includes('you just shot a hole in')) {
recordDailyProfit(false);
state.losses += 1;
state.lastOutcome = 'loss';
save();
render();
if (state.enabled) fillWagerWhenAvailable();
}
}
ui.power.addEventListener('click', () => { state.enabled = !state.enabled; save(); render(); fillWagerWhenAvailable(); });
ui.walletAutofill.addEventListener('change', () => { state.inactiveWalletAutofill = ui.walletAutofill.checked; save(); fillWagerWhenAvailable(); });
ui.ghostTradeToggle.addEventListener('change', () => { state.ghostTradeEnabled = ui.ghostTradeToggle.checked; save(); });
ui.quickJoinToggle.addEventListener('change', () => { state.quickJoinEnabled = ui.quickJoinToggle.checked; if (!state.quickJoinEnabled) removeJoinProxy({ cancelNative: true }); save(); });
ui.multiShotToggle.addEventListener('change', () => { state.hideMultiShotButtons = ui.multiShotToggle.checked; save(); render(); });
ui.turbo2.addEventListener('click', () => startTurbo(2));
ui.turbo3.addEventListener('click', () => startTurbo(3));
ui.turbo2Pill.addEventListener('click', () => startTurbo(2));
ui.turbo3Pill.addEventListener('click', () => startTurbo(3));
ui.reset.addEventListener('click', () => {
state.losses = 0;
state.turboMultiplier = 1;
state.lastOutcome = 'ready';
save();
render();
fillWagerWhenAvailable();
});
ui.base.addEventListener('input', () => { state.base = Math.max(1, Number(ui.base.value) || 1); save(); render(); fillWagerWhenAvailable(); });
ui.multiplier.addEventListener('input', () => { state.multiplier = Math.max(1, Number(ui.multiplier.value) || 1); save(); render(); fillWagerWhenAvailable(); });
ui.base.addEventListener('blur', render);
ui.multiplier.addEventListener('blur', render);
const saveProfitCorrection = () => {
const corrected = Number(String(ui.profitEditor.value).replace(/[^\d.-]/g, ''));
if (!Number.isFinite(corrected)) { renderDailyProfit(); return; }
dailyProfit = Math.round(corrected + Math.max(0, Number(state.dailyExpenses) || 0));
localStorage.setItem(PROFIT_STORE, JSON.stringify({ day: profitDay(), profit: dailyProfit }));
renderDailyProfit();
};
const saveDailyExpenses = () => {
const expenses = Number(String(ui.expenseEditor.value).replace(/[^\d.-]/g, ''));
if (!Number.isFinite(expenses)) { renderDailyProfit(); return; }
state.dailyExpenses = Math.max(0, Math.round(expenses));
save();
renderDailyProfit();
};
ui.profitEditor.addEventListener('blur', saveProfitCorrection);
ui.profitEditor.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); ui.profitEditor.blur(); } });
ui.expenseEditor.addEventListener('blur', saveDailyExpenses);
ui.expenseEditor.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); ui.expenseEditor.blur(); } });
ui.forecastToggle.addEventListener('click', () => { state.forecastOpen = !state.forecastOpen; save(); render(); });
ui.settingsToggle.addEventListener('click', () => { state.settingsOpen = !state.settingsOpen; save(); render(); });
root.querySelector('#rr-minimize').addEventListener('click', () => { state.minimized = true; save(); render(); });
root.querySelector('#rr-expand').addEventListener('click', event => { if (suppressExpand) { event.preventDefault(); suppressExpand = false; return; } state.minimized = false; save(); render(); });
anchoredPill.querySelector('#rr-anchor-expand').addEventListener('click', () => { state.minimized = false; save(); render(); });
anchoredPill.querySelector('#rr-anchor-turbo-2').addEventListener('click', () => startTurbo(2));
anchoredPill.querySelector('#rr-anchor-turbo-3').addEventListener('click', () => startTurbo(3));
window.matchMedia('(max-width: 600px)').addEventListener('change', () => { if (state.minimized) syncMinimizedAnchor(); });
let pendingStartButton = null;
let startConfirmObserver = null;
let joinProxyObserver = null;
function removeJoinProxy({ cancelNative = false } = {}) {
joinProxyObserver?.disconnect();
joinProxyObserver = null;
document.querySelector('#rr-helper-join-proxy')?.remove();
if (pendingStartButton?.confirmation === 'join') {
pendingStartButton = null;
startConfirmObserver?.disconnect();
startConfirmObserver = null;
}
const nativeJoin = [...document.querySelectorAll('button[data-type="confirm"]')]
.find(button => button.textContent.trim().toLowerCase() === 'join');
if (!nativeJoin) return;
nativeJoin.style.removeProperty('visibility');
if (cancelNative) nativeJoin.closest('[class*="confirmWrap"]')?.querySelector('button[data-type="cancel"]')?.click();
}
function showJoinProxy(x, y, gameId) {
removeJoinProxy();
const proxy = document.createElement('button');
proxy.id = 'rr-helper-join-proxy';
proxy.type = 'button';
proxy.textContent = 'Join';
proxy.dataset.rrGameId = gameId;
proxy.style.left = `${x}px`;
proxy.style.top = `${y}px`;
proxy.addEventListener('click', () => {
const nativeJoin = [...document.querySelectorAll('button[data-type="confirm"]')]
.find(button => button.textContent.trim().toLowerCase() === 'join');
if (nativeJoin) { proxy.remove(); nativeJoin.click(); return; }
proxy.dataset.rrWaiting = 'true';
});
document.body.append(proxy);
joinProxyObserver = new MutationObserver(() => {
if (!document.getElementById(proxy.dataset.rrGameId)) removeJoinProxy();
});
joinProxyObserver.observe(document.documentElement, { childList: true, subtree: true });
}
function placeStartConfirmation() {
if (!pendingStartButton) return false;
const confirmButton = [...document.querySelectorAll('button[data-type="confirm"]')]
.find(button => button.textContent.trim().toLowerCase() === pendingStartButton.confirmation);
if (!confirmButton) return false;
if (pendingStartButton.confirmation === 'join') {
confirmButton.style.setProperty('visibility', 'hidden', 'important');
const proxy = document.querySelector('#rr-helper-join-proxy');
if (proxy?.dataset.rrWaiting === 'true') { proxy.remove(); confirmButton.click(); }
} else {
const confirmWrap = confirmButton.closest('[class*="confirmWrap"]');
confirmButton.classList.add('rr-helper-quick-confirm');
confirmWrap?.classList.add('rr-helper-quick-confirm');
const confirmRect = confirmButton.getBoundingClientRect();
const left = Math.max(0, pendingStartButton.x - confirmRect.width / 2);
const top = Math.max(0, pendingStartButton.y - confirmRect.height / 2);
Object.assign(confirmButton.style, {
position: 'fixed',
zIndex: '999999',
left: `${left}px`,
top: `${top}px`,
transform: 'scale(1.5)',
transformOrigin: 'center center'
});
}
pendingStartButton = null;
startConfirmObserver?.disconnect();
startConfirmObserver = null;
return true;
}
document.addEventListener('click', event => {
const joinButton = event.target.closest('button[data-id][class*="submit"]');
const startButton = event.target.closest('button[class*="submit"]');
const confirmation = joinButton ? (state.quickJoinEnabled ? 'join' : null) : startButton && /^\s*start\s*$/i.test(startButton.textContent) ? 'yes' : null;
if (!confirmation) return;
if (joinButton) showJoinProxy(event.clientX, event.clientY, joinButton.dataset.id);
pendingStartButton = { x: event.clientX, y: event.clientY, confirmation };
if (placeStartConfirmation()) return;
startConfirmObserver?.disconnect();
startConfirmObserver = new MutationObserver(placeStartConfirmation);
startConfirmObserver.observe(document.documentElement, { childList: true, subtree: true });
const observerForThisStart = startConfirmObserver;
setTimeout(() => {
if (startConfirmObserver !== observerForThisStart) return;
startConfirmObserver.disconnect();
startConfirmObserver = null;
pendingStartButton = null;
const nativeJoin = [...document.querySelectorAll('button[data-type="confirm"]')]
.find(button => button.textContent.trim().toLowerCase() === 'join');
nativeJoin?.style.removeProperty('visibility');
removeJoinProxy();
}, 5000);
}, true);
document.addEventListener('click', event => {
const proxy = document.querySelector('#rr-helper-join-proxy');
if (!proxy || event.target.closest('#rr-helper-join-proxy') || event.target.closest('button[data-id][class*="submit"]')) return;
removeJoinProxy({ cancelNative: true });
});
applyPosition();
render();
renderMedicalCooldown();
retryWagerFill();
installRrNavigationMenu();
const rrNavObserver = new MutationObserver(scheduleRrNavigationMenu);
rrNavObserver.observe(document.documentElement, { childList: true, characterData: true, attributes: true, attributeFilter: ['href'], subtree: true });
window.addEventListener('hashchange', refreshRrNavigationAfterRouteChange);
window.addEventListener('popstate', refreshRrNavigationAfterRouteChange);
window.addEventListener('hashchange', retryWagerFill);
window.addEventListener('focus', retryWagerFill);
document.addEventListener('visibilitychange', () => { if (!document.hidden) retryWagerFill(); });
setInterval(() => { state = load(); fillWager(); }, 300);
setInterval(renderMedicalCooldown, 1000);
window.addEventListener('storage', event => {
if (event.key !== MEDICAL_STORE) return;
medicalEndTime = Number(event.newValue) || 0;
renderMedicalCooldown();
});
window.addEventListener('storage', event => {
if (event.key !== STORAGE_KEY) return;
state = load();
render();
if (state.enabled) fillWagerWhenAvailable();
});
const seenMessageText = new WeakMap();
const messageSelector = '.messageWrap___WPvHT .message___uTMIk, [class*="messageWrap"] [class*="message"]';
const inspectResultMessage = (message, processNewMessages = true) => {
if (!message?.matches?.(messageSelector)) return;
const text = (message.textContent || '').trim();
if (seenMessageText.get(message) === text) return;
seenMessageText.set(message, text);
if (processNewMessages) handleResult(text);
};
const inspectResultNode = (node, processNewMessages = true) => {
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
if (!element) return;
inspectResultMessage(element.closest(messageSelector), processNewMessages);
element.querySelectorAll?.(messageSelector).forEach(message => inspectResultMessage(message, processNewMessages));
};
document.querySelectorAll(messageSelector).forEach(message => inspectResultMessage(message, false));
const resultObserver = new MutationObserver(records => records.forEach(record => {
inspectResultNode(record.target);
record.addedNodes.forEach(node => inspectResultNode(node));
}));
resultObserver.observe(document.documentElement, { childList: true, characterData: true, subtree: true });
const dragHandles = [root.querySelector('.rr-head'), root.querySelector('.rr-pill')];
let drag = null;
let suppressExpand = false;
function beginDrag(event) {
if (event.currentTarget.classList.contains('rr-head') && event.target.closest('button')) return;
const rect = root.getBoundingClientRect();
drag = { offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top, startX: event.clientX, startY: event.clientY, moved: false };
event.currentTarget.setPointerCapture(event.pointerId);
}
function moveDrag(event) {
if (!drag) return;
if (Math.abs(event.clientX - drag.startX) > 3 || Math.abs(event.clientY - drag.startY) > 3) drag.moved = true;
const rect = root.getBoundingClientRect();
const left = Math.max(0, Math.min(window.innerWidth - rect.width, event.clientX - drag.offsetX));
const top = Math.max(0, Math.min(window.innerHeight - rect.height, event.clientY - drag.offsetY));
root.style.left = `${left}px`;
root.style.top = `${top}px`;
root.style.right = 'auto';
root.style.bottom = 'auto';
}
function endDrag() {
if (!drag) return;
const rect = root.getBoundingClientRect();
state.position = { left: Math.round(rect.left), top: Math.round(rect.top) };
save();
suppressExpand = drag.moved;
drag = null;
}
dragHandles.forEach(handle => {
handle.addEventListener('pointerdown', beginDrag);
handle.addEventListener('pointermove', moveDrag);
handle.addEventListener('pointerup', endDrag);
handle.addEventListener('pointercancel', endDrag);
});
})();