您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
RR Helper Script for various strategies
// ==UserScript== // @name RR Strategy Helper (Beta Test) // @namespace http://tampermonkey.net/ // @version 0.1 // @description RR Helper Script for various strategies // @author Mistral [2717731] // @match *://www.torn.com/page.php?sid=russianRoulette* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_info // ==/UserScript== (function() { 'use strict'; // --- Logging Functions (Debug logging disabled for beta) --- /** No-op function for standard logging (disabled). */ function log(message, ...args) { /* Logging disabled for beta release */ } /** Logs an error to the console and displays it visually. */ function error(message, ...args) { console.error(`[RR Helper ERROR] ${message}`, ...args); // Keep console error for debugging displayError(message); } // log("Script executing..."); // Initial execution log disabled // --- Constants --- const SCRIPT_PREFIX = 'rrHelper_v5.2_'; // Prefix for GM storage const STRATEGIES = { MG: 'Martingale', GMG: 'Grand Martingale', WP: 'Weighted Progression', EXP: 'Exponential (x2.5)' }; const MIN_RISK_LOSSES = 2; const MAX_RISK_LOSSES = 16; const DEFAULT_RISK_LOSSES = 10; const EXP_RATIO = 2.5; const WP_RATIOS = [1, 1.8, 4, 9, 20, 45, 100, 230, 530, 1200, 2750, 6300, 14000, 32000, 72000, 160000]; const ROUNDING_LEVELS = { 1: 'Nearest $1', 100: 'Nearest $100', 1000: 'Nearest $1k', 10000: 'Nearest $10k', 100000: 'Nearest $100k', 1000000: 'Nearest $1M', 10000000: 'Nearest $10M', 100000000: 'Nearest $100M', 1000000000: 'Nearest $1B' }; const DEFAULT_ROUNDING_LEVEL = 1000; const BET_INPUT_SELECTOR = '.createWrap___l0pd7 input[name="bet"]'; const MONEY_BUTTON_SELECTOR = 'button[data-title="Use money from pocket"]'; const MAX_HISTORY = 15; const POT_AMOUNT_SELECTOR = '[class*="potWrap"] [class*="count"]'; const POT_REGEX = /\$([0-9,]+)/; const POT_UPDATE_THROTTLE_MS = 250; const POT_SHARE_RATIO = 0.5; const AUTOFILL_CHECK_INTERVAL = 3000; // Injection & Observation Constants const UI_CONTAINER_ID = 'rr-helper-container-v5.2'; const REACT_ROOT_ID = 'react-root'; const APP_CONTAINER_SELECTOR = '.appContainer___DyC9r'; // Base script had these, keep them in case logic uses them implicitly const APP_WRAPPER_SELECTOR = '.appWrapper___ArZmW'; const LOBBY_CONTENT_SELECTOR = '.createWrap___l0pd7'; // --- const CONTENT_WRAPPER_SELECTOR = '.content-wrapper'; const VIEW_CHANGE_DEBOUNCE_MS = 300; const ACTION_FEEDBACK_DURATION_MS = 300; const ERROR_DISPLAY_DURATION_MS = 5000; // --- State Variables --- let state = { bankroll: 300000000, riskLevel: DEFAULT_RISK_LOSSES, strategy: 'WP', streak: 0, sessionProfitLoss: 0, baseBet: 0, nextBet: 0, isAffordable: true, initialized: false, gameHistory: [], autoPotShare: 0, detectedPot: 0, potDetectionStatus: 'Initializing...', selectedAmountMode: 'recommended', isSettingsCollapsed: true, roundingLevel: DEFAULT_ROUNDING_LEVEL, disableAutofill: false, }; let autofillIntervalId = null; let potObserver = null; let potUpdateTimeout = null; let viewChangeObserver = null; let viewChangeTimeout = null; let errorTimeout = null; // --- Utility Functions --- function parseMoney(moneyString) { if (typeof moneyString === 'number') return Math.max(0, Math.floor(moneyString)); if (typeof moneyString !== 'string') return 0; const parsed = parseInt(String(moneyString).replace(/[^0-9]/g, ''), 10); return isNaN(parsed) ? 0 : parsed; } function roundToNearestLevel(amount, level) { const numericLevel = Number(level); if (!isFinite(amount) || amount === 0 || !isFinite(numericLevel) || numericLevel <= 0) return Math.floor(amount || 0); const rounded = Math.round(amount / numericLevel) * numericLevel; const minBet = 1000; return (amount >= minBet && rounded < minBet && numericLevel >= minBet) ? minBet : rounded; } function formatMoney(amount, forceSign = false) { const numericAmount = Number(amount); if (numericAmount === undefined || numericAmount === null || !isFinite(numericAmount)) return (numericAmount === Infinity) ? '∞' : '$?'; const absAmount = Math.abs(numericAmount); const roundedAbs = roundToNearestLevel(absAmount, state.roundingLevel); const formatted = '$' + roundedAbs.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 }); return forceSign ? (numericAmount >= 0 ? '+' : '-') + formatted : formatted; } function formatPlAmount(amount) { const numericAmount = Number(amount); if (numericAmount === undefined || numericAmount === null || !isFinite(numericAmount)) return '+/- $?'; const absAmount = Math.abs(numericAmount).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 }); return (numericAmount >= 0 ? '+' : '-') + '$' + absAmount; } function displayError(message, isWarning = false) { log(isWarning ? "Warning Displayed:" : "Error Displayed:", message); const errorDiv = document.getElementById('rrErrorDisplay'); if (!errorDiv) { console.error("Error display element not found!"); if (!isWarning) alert(`ERROR: ${message}`); return; } errorDiv.textContent = message; errorDiv.className = `rr-error-display ${isWarning ? 'warning' : 'error'} visible`; clearTimeout(errorTimeout); errorTimeout = setTimeout(() => { errorDiv.classList.remove('visible'); }, ERROR_DISPLAY_DURATION_MS); } function clearError() { const errorDiv = document.getElementById('rrErrorDisplay'); if(errorDiv) errorDiv.classList.remove('visible'); clearTimeout(errorTimeout); } // --- GM Storage Functions --- function saveState() { try { GM_setValue(SCRIPT_PREFIX + 'state', JSON.stringify(state)); } catch (e) { error("Failed to save state", e); } } function loadState() { try { const s = GM_getValue(SCRIPT_PREFIX + 'state', '{}'); if (s) { Object.assign(state, JSON.parse(s)); } } catch (e) { error("Failed to load state", e); GM_setValue(SCRIPT_PREFIX + 'state', '{}'); /* Reset corrupted state */ } } // --- Session & History Management --- function resetSession() { log("Resetting session..."); clearError(); state.streak = 0; state.sessionProfitLoss = 0; state.gameHistory = []; if (state.initialized) { calculateBets(); saveState(); updateUI(); } log("Session Reset Complete."); } function applyProfitLossToBankroll() { log("Applying P/L to Bankroll..."); clearError(); const newBankroll = Number(state.bankroll) + Number(state.sessionProfitLoss); if (!isFinite(newBankroll)) { error("Cannot apply P/L: Resulting bankroll is not finite."); return; } log(`Current Bankroll: ${formatMoney(state.bankroll)}, Session P/L: ${formatPlAmount(state.sessionProfitLoss)}, New Bankroll: ${formatMoney(newBankroll)}`); state.bankroll = newBankroll; state.sessionProfitLoss = 0; if (state.initialized) { calculateBets(); saveState(); updateUI(); } log("P/L Applied."); } function addGameToHistory(outcome, amount, currentPlBefore) { log(`Adding game to history: outcome=${outcome}, amount=${formatMoney(amount)}, pLBefore=${formatPlAmount(currentPlBefore)}`); if (!outcome || !isFinite(amount)) { error("Invalid outcome or amount for history add.", {outcome, amount}); return; } state.gameHistory.unshift({ outcome: outcome, bet: amount, timestamp: Date.now(), plBefore: currentPlBefore }); if (state.gameHistory.length > MAX_HISTORY) state.gameHistory.pop(); log(`History length now: ${state.gameHistory.length}`); } function handleRemoveLastGame() { if (!state.initialized || state.gameHistory.length === 0) { log("History empty."); displayError("No history entries to remove.", true); return; } log("Attempting to remove last game..."); clearError(); const lastGame = state.gameHistory.shift(); if (!lastGame || !lastGame.outcome || !isFinite(lastGame.bet) || !isFinite(lastGame.plBefore)) { error("Invalid history data found during removal.", lastGame); calculateBets(); saveState(); updateUI(); return; } log("Removed game:", lastGame); state.sessionProfitLoss = lastGame.plBefore; log(`Reverted P/L to ${formatPlAmount(state.sessionProfitLoss)}`); let newStreak = 0; if (state.gameHistory.length > 0) { const lastValid = state.gameHistory.find(g => g.outcome === 'win' || g.outcome === 'loss'); if (lastValid) { const lastOutcome = lastValid.outcome; let count = 0; for (const g of state.gameHistory) { if (g.outcome === lastOutcome) count++; else if (g.outcome === 'win' || g.outcome === 'loss') break; } newStreak = (lastOutcome === 'win') ? count : -count; } } log(`Reverted streak: Old was ${state.streak}, New is ${newStreak}`); state.streak = newStreak; calculateBets(); saveState(); updateUI(); log("Last game removal complete."); } function getCurrentAmount() { if (!state.initialized) return 0; let amount = 0; if (state.selectedAmountMode === 'auto') amount = state.autoPotShare > 0 ? state.autoPotShare : 0; else if (state.selectedAmountMode === 'recommended') amount = isFinite(state.nextBet) && state.nextBet > 0 ? state.nextBet : 0; else { const c = document.getElementById('rrCustomAmountInput'); amount = c ? parseMoney(c.value) : 0; } return amount; } function handleOutcomeClick(outcome) { log(`Outcome button clicked: ${outcome}`); if (!state.initialized) { error("Cannot record outcome: Script not initialized."); return; } clearError(); const button = document.getElementById(`rr${outcome.charAt(0).toUpperCase() + outcome.slice(1)}Button`); if (!button || button.disabled) { log(`Outcome button ${outcome} is disabled or not found.`); displayError("Cannot record outcome: Action disabled or amount invalid.", true); return; } let amount = getCurrentAmount(); let amountForHandler = amount; let errorMsg = null; if (outcome === 'win' || outcome === 'loss') { if (!isFinite(amount) || amount <= 0) { if (state.selectedAmountMode === 'custom') errorMsg = "Win/Loss requires positive Custom Amount."; else if (state.selectedAmountMode === 'auto') errorMsg = "Win/Loss requires valid Auto Pot share (> $0)."; else if (state.selectedAmountMode === 'recommended') errorMsg = "Win/Loss requires valid Recommended Bet (> $0)."; else errorMsg = "Invalid amount for Win/Loss."; } else amountForHandler = amount; } else if (outcome === 'mug') { if (state.selectedAmountMode !== 'custom') errorMsg = "Mug requires Custom mode."; else if (!isFinite(amount) || amount <= 0) { errorMsg = "Mug requires positive amount lost in Custom Amount."; document.getElementById('rrCustomAmountInput')?.focus(); } else amountForHandler = -Math.abs(amount); } else { error("Unknown outcome button type:", outcome); return; } if (errorMsg) { displayError(errorMsg); return; } log(`Recording Outcome: ${outcome.toUpperCase()} | Mode: ${state.selectedAmountMode} | Amount: ${formatMoney(Math.abs(amountForHandler))} (Handler value: ${amountForHandler})`); if (button) { button.classList.add('rr-button-clicked'); setTimeout(() => button.classList.remove('rr-button-clicked'), ACTION_FEEDBACK_DURATION_MS); } handleGameResult(outcome, amountForHandler); updateUI(); } // --- Pot Detection --- function findPotAmountElement() { return document.querySelector(POT_AMOUNT_SELECTOR); } function updatePotDetectionState(status, detectedPot = 0, autoShare = 0) { if (state.potDetectionStatus !== status || state.detectedPot !== detectedPot || state.autoPotShare !== autoShare) { state.potDetectionStatus = status; state.detectedPot = detectedPot; state.autoPotShare = autoShare; if(state.initialized) updateUI(); } } function updateAutoPotShare(potElement) { if (!potElement || !state.initialized) { if (state.potDetectionStatus !== 'Not Found') { updatePotDetectionState('Not Found', 0, 0); if (state.selectedAmountMode === 'auto') { log("Pot element gone, switching mode from Auto to Recommended."); state.selectedAmountMode = 'recommended'; updateUI(); } } return; } const text = potElement.textContent || ""; const match = text.match(POT_REGEX); const currentPot = match ? parseMoney(match[1]) : 0; const share = currentPot > 0 ? roundToNearestLevel(currentPot * POT_SHARE_RATIO, state.roundingLevel) : 0; const finalShare = (currentPot >= 2000 && share < 1000) ? 1000 : share; if (finalShare !== state.autoPotShare || currentPot !== state.detectedPot) { log(`Pot update: Text='${text}', Pot=${formatMoney(currentPot)}, Share=${formatMoney(finalShare)}`); const newStatus = (currentPot > 0) ? `Active: ${formatMoney(currentPot)}` : 'Not Found'; updatePotDetectionState(newStatus, currentPot, finalShare); const display = document.getElementById('rrAutoPotDisplay'); if (display) { display.textContent = formatMoney(finalShare); display.title = `Detected Pot: ${formatMoney(currentPot)}\nShare (est.): ${formatMoney(finalShare)}`; } if (finalShare > 0) { log("Valid pot share detected, switching mode to Auto."); state.selectedAmountMode = 'auto'; updateUI(); } else if (finalShare <= 0 && state.selectedAmountMode === 'auto') { log("Auto Pot Share became zero/invalid, switching to Recommended."); state.selectedAmountMode = 'recommended'; updateUI(); } } else { const expectedStatus = (currentPot > 0) ? `Active: ${formatMoney(currentPot)}` : 'Not Found'; if (state.potDetectionStatus !== expectedStatus) updatePotDetectionState(expectedStatus, currentPot, finalShare); } } function observePotChanges() { log("Setting up Pot observer..."); if (potObserver) { potObserver.disconnect(); log("Previous Pot observer disconnected."); potObserver = null; } const potElement = findPotAmountElement(); if (!potElement) { log("Cannot observe pot: Pot amount element not found initially."); updatePotDetectionState('Not Found', 0, 0); if (state.selectedAmountMode === 'auto') state.selectedAmountMode = 'recommended'; if(state.initialized) updateUI(); return; } updatePotDetectionState('Detecting...', state.detectedPot, state.autoPotShare); updateAutoPotShare(potElement); const observerCallback = (mutationsList, observer) => { let changed = false; for (const mutation of mutationsList) { if ((mutation.type === 'characterData' && mutation.target === potElement) || (mutation.type === 'childList' && (mutation.target === potElement.parentElement || mutation.target === potElement))) { changed = true; break; } } if (changed) { clearTimeout(potUpdateTimeout); potUpdateTimeout = setTimeout(() => { const currentEl = findPotAmountElement(); if (currentEl && document.body.contains(currentEl)) { updateAutoPotShare(currentEl); } else { log("Pot amount element disappeared or detached during observation."); observer.disconnect(); potObserver = null; updatePotDetectionState('Not Found', 0, 0); if (state.selectedAmountMode === 'auto') state.selectedAmountMode = 'recommended'; if(state.initialized) updateUI(); } }, POT_UPDATE_THROTTLE_MS); } }; potObserver = new MutationObserver(observerCallback); const targetNode = potElement; const parentNode = potElement.parentElement; const config = { characterData: true, subtree: false, childList: false }; const parentConfig = { childList: true, subtree: false }; try { potObserver.observe(targetNode, config); if (parentNode) potObserver.observe(parentNode, parentConfig); potObserver.targetNode = targetNode; log("Pot observer started.", {targetNode, parentNode}); updatePotDetectionState('Monitoring', state.detectedPot, state.autoPotShare); } catch (e) { error("Failed to start pot observer", e, {targetNode, parentNode}); updatePotDetectionState('Error', 0, 0); } } // --- Calculation Functions --- function getCumulativeRatioSum(strategy, N_losses) { let s=0; if(N_losses<=0) return 0; if(strategy==='MG') s=Math.pow(2, N_losses)-1; else if(strategy==='GMG') s=Math.pow(2, N_losses+1)-2-N_losses; else if(strategy==='WP'){ for(let i=0; i<N_losses && i<WP_RATIOS.length; i++) s+=WP_RATIOS[i]; if(N_losses>WP_RATIOS.length) return Infinity;} else if (strategy === 'EXP') { if (EXP_RATIO <= 1) return Infinity; s = (Math.pow(EXP_RATIO, N_losses) - 1) / (EXP_RATIO - 1); if (!isFinite(s)) return Infinity; } return Math.max(0,s); } function getBetRatio(strategy, lossStreakIndex) { let ratio = 1; if(lossStreakIndex < 0) return ratio; if(strategy==='MG') ratio = Math.pow(2, lossStreakIndex); else if(strategy==='GMG') ratio = Math.pow(2, lossStreakIndex+1)-1; else if(strategy==='WP') ratio = (lossStreakIndex>=0 && lossStreakIndex<WP_RATIOS.length)?WP_RATIOS[lossStreakIndex]:Infinity; else if (strategy === 'EXP') ratio = Math.pow(EXP_RATIO, lossStreakIndex); return ratio; } function calculateBaseBet() { log("Calculating Base Bet..."); const N = Number(state.riskLevel) || DEFAULT_RISK_LOSSES; const currentBankroll = Number(state.bankroll); if (N < MIN_RISK_LOSSES || !isFinite(currentBankroll) || currentBankroll <= 0) { log(`Invalid input for base bet calc. Setting base bet to minimum.`); state.baseBet = 1000; calculateNextBetAmount(); return; } const sum = getCumulativeRatioSum(state.strategy, N); let rawBaseBet = 1; if (sum > 0 && isFinite(sum)) { rawBaseBet = Math.max(1, Math.floor(currentBankroll / sum)); } else if (sum === 0) { rawBaseBet = Math.max(1, Math.floor(currentBankroll * 0.01)); } else { rawBaseBet = 1000; error(`Strategy ${state.strategy} cannot sustain ${N} losses. Base bet forced to minimum.`, true); } state.baseBet = roundToNearestLevel(rawBaseBet, state.roundingLevel); if (state.baseBet < 1000 && rawBaseBet >= 1) { log(`Base bet rounded below minimum ($${formatMoney(state.baseBet)}), enforcing $1000.`); state.baseBet = 1000; } else if (state.baseBet === 0 && rawBaseBet > 0) { log(`Base bet rounded to zero, enforcing $1000.`); state.baseBet = 1000; } log(`Final Base Bet: ${formatMoney(state.baseBet)}`); calculateNextBetAmount(); } function calculateNextBetAmount() { log("Calculating Next Bet Amount..."); const baseBet = Number(state.baseBet); const currentStreak = Number(state.streak); const currentBankroll = Number(state.bankroll); const currentPL = Number(state.sessionProfitLoss); if (baseBet <= 0 || !isFinite(baseBet)) { log(`Invalid base bet ($${baseBet}). Setting next bet to 0.`); state.nextBet = 0; state.isAffordable = false; return; } const lossStreakIndex = currentStreak < 0 ? Math.abs(currentStreak) : 0; const ratio = (currentStreak >= 0) ? 1 : getBetRatio(state.strategy, lossStreakIndex); let rawNextBet = 0; if (!isFinite(ratio)) { log(`Max loss streak reached or invalid ratio for strategy ${state.strategy} (Index ${lossStreakIndex}).`); error(`Max loss streak reached (${Math.abs(currentStreak)} losses). Consider adjusting strategy, risk, or bankroll.`, true); state.nextBet = Infinity; } else { rawNextBet = Math.max(1, Math.floor(baseBet * ratio)); state.nextBet = roundToNearestLevel(rawNextBet, state.roundingLevel); if (state.nextBet < 1000 && rawNextBet >= 1) { log(`Next bet rounded below minimum ($${formatMoney(state.nextBet)}), enforcing $1000.`); state.nextBet = 1000; } else if (state.nextBet === 0 && rawNextBet > 0) { log(`Next bet rounded to zero, enforcing $1000.`); state.nextBet = 1000; } } const capital = currentBankroll + currentPL; state.isAffordable = isFinite(state.nextBet) ? state.nextBet <= capital : false; log(`Final Next Bet: ${formatMoney(state.nextBet)}. Affordable: ${state.isAffordable} (Capital: ${formatMoney(capital)})`); } function calculateBets() { log("calculateBets called."); clearError(); calculateBaseBet(); log("calculateBets finished."); } // --- UI Functions --- /** Creates the helper UI DOM element but does not inject it. */ function createHelperUI() { log("Creating Helper UI Element..."); const uiContainer = document.createElement('div'); uiContainer.id = UI_CONTAINER_ID; uiContainer.className = 'rr-helper'; let riskOptions = ''; for (let i = MIN_RISK_LOSSES; i <= MAX_RISK_LOSSES; i++) { riskOptions += `<option value="${i}" ${state.riskLevel === i ? 'selected' : ''}>${i} Losses</option>`; } let strategyOptions = Object.entries(STRATEGIES).map(([k, v]) => `<option value="${k}" ${state.strategy === k ? 'selected' : ''}>${v}</option>`).join(''); let roundingOptions = Object.entries(ROUNDING_LEVELS).map(([level, text]) => `<option value="${level}" ${Number(state.roundingLevel) === Number(level) ? 'selected' : ''}>${text}</option>`).join(''); uiContainer.innerHTML = ` <div id="rrErrorDisplay" class="rr-error-display"></div> <div class="rr-top-bar"> <span class="rr-bankroll-display"> Bankroll: <span id="rrBankrollDisplay" title="Your base bankroll setting">$?</span> (<span id="rrEffectiveBankrollDisplay" title="Bankroll + Session P/L">$?</span> Effective) </span> <button id="rrSettingsToggle" class="rr-settings-button" title="Toggle Settings">⋮ Settings</button> </div> <div class="rr-next-bet-area"> <div class="rr-next-bet-label"> <span>NEXT BET <span id="rrNextBetAffordableIndicator" class="rr-affordable-indicator"></span></span> <span class="rr-streak-display">(<span id="rrStreakDisplay">W0</span>)</span> <span class="rr-pl-display" title="Current Session Profit/Loss">P/L <span id="rrSessionPLDisplay">+$0</span></span> </div> <div class="rr-next-bet-value-wrapper"> <span id="rrNextBetValue" class="rr-next-bet-value" title="Recommended next bet based on settings">$?</span> <button id="rrFillBetButton" class="rr-fill-button" title="Fill Torn Bet Input" disabled>FILL</button> </div> </div> <div class="rr-outcome-controls"> <div class="rr-amount-mode"> <label id="rrLabelAuto" class="rr-radio-label" title="Use calculated 50% share of the detected game pot"> <input type="radio" name="rrAmountMode" id="rrModeAuto" value="auto"> Auto pot <span class="rr-auto-pot-value" id="rrAutoPotDisplay">$?</span> <span id="rrPotStatus" class="rr-pot-status"></span> </label> <label id="rrLabelRecommended" class="rr-radio-label" title="Use the Next Recommended Bet calculated above"> <input type="radio" name="rrAmountMode" id="rrModeRecommended" value="recommended" checked> Recommended </label> <label id="rrLabelCustom" class="rr-radio-label" title="Use a manually entered amount"> <input type="radio" name="rrAmountMode" id="rrModeCustom" value="custom"> Custom </label> <input id="rrCustomAmountInput" class="rr-custom-amount-input" type="text" inputmode="numeric" pattern="[0-9,]*" placeholder="Manual Amount" disabled> </div> <div class="rr-action-buttons"> <button id="rrWinButton" class="rr-outcome-button rr-win-button" disabled>WIN<span></span></button> <button id="rrLossButton" class="rr-outcome-button rr-loss-button" disabled>LOSS<span></span></button> <button id="rrMugButton" class="rr-outcome-button rr-mug-button" disabled>MUG<span></span></button> </div> </div> <div id="rrSettingsSection" class="rr-settings-section ${state.isSettingsCollapsed ? 'rr-settings-hidden' : ''}"> <hr class="rr-divider"> <h5>Settings</h5> <div class="rr-settings-grid"> <label for="rrBankrollInput">Bankroll:</label> <input type="text" inputmode="numeric" pattern="[0-9,]*" id="rrBankrollInput" title="Set your starting/current bankroll"> <label>Effective Bankroll:</label> <span id="rrEffectiveBankrollSettingsDisplay" class="rr-calculated-bankroll" title="Bankroll + Session P/L (Informational)">$?</span> <label for="rrRiskLevelSelect">Risk Level:</label> <select id="rrRiskLevelSelect" title="Max consecutive losses the strategy should aim to cover">${riskOptions}</select> <label for="rrStrategySelect">Strategy:</label> <select id="rrStrategySelect" title="Select the betting progression strategy">${strategyOptions}</select> <label for="rrRoundingLevelSelect">Rounding:</label> <select id="rrRoundingLevelSelect" title="Round calculated bets to the nearest selected value">${roundingOptions}</select> <label for="rrDisableAutofillCheckbox">Disable Autofill:</label> <input type="checkbox" id="rrDisableAutofillCheckbox" title="Prevent automatically filling the bet input if it contains less than $100"> <div class="rr-settings-buttons"> <button id="rrExportCSVButton" class="torn-btn alternate rr-action-button" title="Export settings and history to a CSV file">Export CSV</button> <button id="rrApplyPLButton" class="torn-btn alternate rr-action-button" title="Add Session P/L to Bankroll and reset P/L to $0">Apply P/L to Bankroll</button> <button id="rrResetSessionButton" class="torn-btn alternate rr-action-button" title="Reset Session P/L, Streak, and History">Reset Session</button> </div> </div> </div> <div class="rr-history-section"> <hr class="rr-divider"> <h5>Recent Entries <button id="rrRemoveLastEntryButton" class="rr-remove-button" title="Remove Last Entry" disabled>⌫</button></h5> <ul id="rrHistoryList"><li>No entries recorded yet.</li></ul> </div> `; addEventListeners(uiContainer); log("Helper UI Element created."); return uiContainer; } /** Checks if the UI exists and injects/moves it if necessary. Ensures it's appended within App Container. */ function ensureHelperUIVisible() { if (!state.initialized) return; const reactRoot = document.getElementById(REACT_ROOT_ID); if (!reactRoot) { log("No react root (ensure)"); return; } const appContainer = reactRoot.querySelector(APP_CONTAINER_SELECTOR); if (!appContainer) { log("No app container (ensure)"); return; } let helperUI = document.getElementById(UI_CONTAINER_ID); if (!helperUI) { log("Helper UI injecting (appending)..."); helperUI = createHelperUI(); appContainer.appendChild(helperUI); // Append to ensure last position log("Helper UI appended to app container."); updateUI(); } else if (helperUI.parentElement !== appContainer) { log("Helper UI moving to correct parent (appending)..."); appContainer.appendChild(helperUI); // Move by appending updateUI(); } else if (appContainer.lastElementChild !== helperUI) { // If it's in the right parent but not last, move it to the end log("Helper UI ensuring last position (appending)..."); appContainer.appendChild(helperUI); // Re-append to move to end updateUI(); } else { // Already exists and correctly placed updateUI(); } // Ensure Pot Observer is Running (if not already, or if target detached) // Use optional chaining ?. for safety in case potObserver is null if (!potObserver || (potObserver?.targetNode && !document.body.contains(potObserver.targetNode))) { log("Re-checking/starting Pot Observer in ensureUI..."); observePotChanges(); } } function updateHistoryUI() { const h=document.getElementById('rrHistoryList'); if(!h) return; if(state.gameHistory.length===0){ h.innerHTML='<li>No entries recorded yet.</li>'; return; } h.innerHTML=''; state.gameHistory.forEach(g=>{ const li=document.createElement('li'); const o=`rr-history-${g.outcome}`; li.className=o; const a=(g.outcome==='mug')?`-$${Math.abs(g.bet).toLocaleString()}`:formatMoney(g.bet); li.textContent=`${g.outcome.toUpperCase()} - ${a}`; li.title = `Timestamp: ${new Date(g.timestamp).toLocaleString()}\nP/L Before: ${formatPlAmount(g.plBefore)}`; h.appendChild(li); }); } function addEventListeners(container) { log("Adding event listeners..."); container.querySelector('#rrSettingsToggle').addEventListener('click', () => { log("Settings toggle clicked."); state.isSettingsCollapsed = !state.isSettingsCollapsed; document.getElementById('rrSettingsSection')?.classList.toggle('rr-settings-hidden', state.isSettingsCollapsed); saveState(); }); const bankrollInput = container.querySelector('#rrBankrollInput'); bankrollInput.addEventListener('change', (e) => { log("Bankroll input changed."); state.bankroll = parseMoney(e.target.value); if (state.bankroll < 0) state.bankroll = 0; e.target.value = state.bankroll.toLocaleString(); log(`New Bankroll value: ${state.bankroll}`); calculateBets(); saveState(); updateUI(); }); container.querySelector('#rrRiskLevelSelect').addEventListener('change', (e) => { state.riskLevel = Number(e.target.value); log(`Risk level changed to: ${state.riskLevel}`); calculateBets(); saveState(); updateUI(); }); container.querySelector('#rrStrategySelect').addEventListener('change', (e) => { state.strategy = e.target.value; log(`Strategy changed to: ${state.strategy}`); calculateBets(); saveState(); updateUI(); }); container.querySelector('#rrRoundingLevelSelect').addEventListener('change', (e) => { state.roundingLevel = Number(e.target.value); log(`Rounding level changed to: ${state.roundingLevel}`); calculateBets(); saveState(); updateUI(); }); container.querySelector('#rrDisableAutofillCheckbox').addEventListener('change', (e) => { state.disableAutofill = e.target.checked; log(`Disable Autofill changed to: ${state.disableAutofill}`); saveState(); updateUI(); }); container.querySelector('#rrResetSessionButton').addEventListener('click', resetSession); container.querySelector('#rrApplyPLButton').addEventListener('click', applyProfitLossToBankroll); container.querySelector('#rrExportCSVButton').addEventListener('click', exportDataToCSV); container.querySelector('#rrFillBetButton').addEventListener('click', handleFillButtonClick); container.querySelector('#rrWinButton').addEventListener('click', () => handleOutcomeClick('win')); container.querySelector('#rrLossButton').addEventListener('click', () => handleOutcomeClick('loss')); container.querySelector('#rrMugButton').addEventListener('click', () => handleOutcomeClick('mug')); container.querySelector('#rrModeAuto').addEventListener('change', handleAmountModeChange); container.querySelector('#rrModeRecommended').addEventListener('change', handleAmountModeChange); container.querySelector('#rrModeCustom').addEventListener('change', handleAmountModeChange); const customInput = container.querySelector('#rrCustomAmountInput'); customInput.addEventListener('input', (e) => { updateUI(); }); customInput.addEventListener('change', (e) => { log("Custom amount change event"); let val = parseMoney(e.target.value); e.target.value = val > 0 ? val.toLocaleString() : ''; updateUI(); }); customInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { log("Enter pressed in custom input."); e.preventDefault(); const btn = document.getElementById('rrWinButton'); if (btn && !btn.disabled) { log("Enter in custom -> Triggering WIN"); handleOutcomeClick('win'); } else { log("Enter in custom -> Win button disabled."); } } }); container.querySelector('#rrRemoveLastEntryButton').addEventListener('click', handleRemoveLastGame); attachMoneyButtonListener(); log("Event listeners added."); } function handleAmountModeChange(event) { if (!event.target.checked || !state.initialized) return; state.selectedAmountMode = event.target.value; log(`Amount mode changed to: ${state.selectedAmountMode}`); clearError(); const cI = document.getElementById('rrCustomAmountInput'); if (cI) { const isDisabled = (state.selectedAmountMode !== 'custom'); cI.disabled = isDisabled; if (!isDisabled) { log("Focusing custom input."); const prevModeVal = state.selectedAmountMode === 'auto' ? state.autoPotShare : (isFinite(state.nextBet) ? state.nextBet : ''); cI.value = prevModeVal > 0 ? prevModeVal.toLocaleString() : ''; setTimeout(() => { cI.focus(); cI.select(); }, 50); } } updateUI(); saveState(); } function attachMoneyButtonListener() { log("Attempting to attach listener to TORN money button..."); let attempts = 0, maxAttempts = 20, interval = 500; const findButtonInterval = setInterval(() => { attempts++; const btn = document.querySelector(MONEY_BUTTON_SELECTOR); if (btn) { clearInterval(findButtonInterval); if (!btn.hasAttribute('data-rr-listener-v5.2')) { btn.addEventListener('click', handleTornMoneyButtonClick); btn.setAttribute('data-rr-listener-v5.2', 'true'); log("Listener attached to TORN money button."); } } else if (attempts >= maxAttempts) { clearInterval(findButtonInterval); log(`Could not find TORN money button (${MONEY_BUTTON_SELECTOR}) after ${maxAttempts} attempts.`); } }, interval); } function handleFillButtonClick(event) { log("Fill button clicked."); clearError(); updateBetInput(true); } function handleTornMoneyButtonClick(event) { log("TORN Money button clicked."); clearError(); setTimeout(() => { updateBetInput(true); }, 50); } function dispatchEvent(element, eventType) { try { log(`Dispatching ${eventType} event on`, element); const e = new Event(eventType, { bubbles: true, cancelable: true }); element.dispatchEvent(e); } catch (err) { error(`Error dispatching ${eventType} event`, err); } } /** Updates the bet input field IF manually triggered OR (if enabled) if the current value is < 100 or empty. */ function updateBetInput(manualTrigger = false) { log(`updateBetInput: Manual=${manualTrigger}, AutofillDisabled=${state.disableAutofill}`); if (!state.initialized) { log("updateBetInput skipped: Not initialized."); return; } const i=document.querySelector(BET_INPUT_SELECTOR); if (!i) { if (manualTrigger) displayError("Bet input field not found for fill action."); else log("Bet input field not found, skipping update."); return; } const currentValNumber = parseMoney(i.value || '0'); const isDefaultPlaceholder = !i.value || currentValNumber < 100; const allowAutofill = !state.disableAutofill && isDefaultPlaceholder; if (!manualTrigger && !allowAutofill) { log("updateBetInput skipped: Not manual trigger and autofill condition not met."); return; } let rec=0; const nextOK=isFinite(state.nextBet) && state.nextBet>0; const baseOK=isFinite(state.baseBet) && state.baseBet>0; if(nextOK) rec=state.nextBet; else if(baseOK) { const cap=Number(state.bankroll)+Number(state.sessionProfitLoss); rec=(state.baseBet<=cap)?state.baseBet:1000; } else rec=1000; const val=String(rec); if(i.value !== val || manualTrigger){ if(manualTrigger) log(`Manual set: ${val}`); else if(allowAutofill) log(`Autofill (< 100): ${val}`); try { const s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set; if (s) { s.call(i, val); dispatchEvent(i, 'input'); dispatchEvent(i, 'change'); } else throw new Error("No setter"); } catch(e){ error("Native set error:", e); i.value=val; requestAnimationFrame(()=>{ dispatchEvent(i, 'input'); dispatchEvent(i, 'change'); }); } i.style.transition = 'background-color 0.1s ease-in-out'; i.style.backgroundColor = '#aaffaa'; setTimeout(() => { i.style.backgroundColor = ''; }, 200); } else log("Bet input value already matches target value, no update needed."); } /** Updates the entire UI based on the current state */ function updateUI() { if (!state.initialized) { return; } const container = document.getElementById(UI_CONTAINER_ID); if (!container) { return; } try { // Top Bar container.querySelector('#rrBankrollDisplay').textContent = formatMoney(state.bankroll); container.querySelector('#rrEffectiveBankrollDisplay').textContent = formatMoney(Number(state.bankroll) + Number(state.sessionProfitLoss)); // Next Bet Area const nextBetValueEl = container.querySelector('#rrNextBetValue'); nextBetValueEl.textContent = formatMoney(state.nextBet); nextBetValueEl.title = `Base: ${formatMoney(state.baseBet)} | Strat: ${STRATEGIES[state.strategy] || state.strategy} | Risk: ${state.riskLevel} | Streak: ${state.streak}`; const affordIndicator = container.querySelector('#rrNextBetAffordableIndicator'); affordIndicator.textContent = (!state.isAffordable && isFinite(state.nextBet) && state.nextBet > 0) ? '(Exceeds Capital)' : ''; affordIndicator.title = affordIndicator.textContent ? `Next bet (${formatMoney(state.nextBet)}) exceeds effective capital (${formatMoney(Number(state.bankroll) + Number(state.sessionProfitLoss))})` : ''; const streakDisplay = state.streak > 0 ? `W${state.streak}` : (state.streak < 0 ? `L${Math.abs(state.streak)}` : 'W0'); container.querySelector('#rrStreakDisplay').textContent = streakDisplay; const plAmount = Number(state.sessionProfitLoss); const plDisplay = container.querySelector('#rrSessionPLDisplay'); plDisplay.textContent = formatPlAmount(plAmount); const plOuterSpan = plDisplay.closest('.rr-pl-display'); if (plOuterSpan) { plOuterSpan.className = `rr-pl-display ${plAmount > 0 ? 'positive' : (plAmount < 0 ? 'negative' : '')}`; } // Outcome Controls container.querySelector('#rrAutoPotDisplay').textContent = formatMoney(state.autoPotShare); const potStatusEl = container.querySelector('#rrPotStatus'); let statusText = ''; let statusClass = ''; switch(state.potDetectionStatus) { case 'Monitoring': statusText = ''; statusClass = 'status-watching'; break; case 'Not Found': statusText = '(N/A)'; statusClass = 'status-not-found'; break; case 'Error': statusText = '(Err)'; statusClass = 'status-error'; break; default: statusText = `(${state.potDetectionStatus})`; statusClass = 'status-init'; break; } potStatusEl.textContent = statusText; potStatusEl.className = `rr-pot-status ${statusClass}`; potStatusEl.title = `Detected Pot: ${formatMoney(state.detectedPot)} | Calc Share: ${formatMoney(state.autoPotShare)}`; const autoRadio = container.querySelector('#rrModeAuto'); autoRadio.disabled = !state.autoPotShare || state.autoPotShare <= 0; if (state.selectedAmountMode === 'auto') autoRadio.checked = true; const recRadio = container.querySelector('#rrModeRecommended'); recRadio.checked = state.selectedAmountMode === 'recommended'; const customRadio = container.querySelector('#rrModeCustom'); customRadio.checked = state.selectedAmountMode === 'custom'; const customInput = container.querySelector('#rrCustomAmountInput'); customInput.disabled = (state.selectedAmountMode !== 'custom'); // Highlight Active Source container.querySelectorAll('.rr-active-source').forEach(el => el.classList.remove('rr-active-source')); if (state.selectedAmountMode === 'auto') container.querySelector('#rrAutoPotDisplay')?.classList.add('rr-active-source'); else if (state.selectedAmountMode === 'recommended') nextBetValueEl?.classList.add('rr-active-source'); else if (state.selectedAmountMode === 'custom') customInput?.classList.add('rr-active-source'); // Action Buttons State & Text const currentAmount = getCurrentAmount(); let isCurrentAmountValid = false; let winLossDisabledReason = "Select valid amount source"; let mugDisabledReason = "Select Custom mode and enter positive amount lost"; let buttonAmountText = ""; if (state.selectedAmountMode === 'recommended') { isCurrentAmountValid = isFinite(state.nextBet) && state.nextBet > 0; if(isCurrentAmountValid) buttonAmountText = formatMoney(currentAmount); else winLossDisabledReason = "Recommended Bet is invalid or zero"; } else if (state.selectedAmountMode === 'auto') { isCurrentAmountValid = state.autoPotShare > 0; if(isCurrentAmountValid) buttonAmountText = formatMoney(currentAmount); else winLossDisabledReason = "Auto Pot Share is zero or unavailable"; } else { isCurrentAmountValid = currentAmount > 0; if(isCurrentAmountValid) { buttonAmountText = formatMoney(currentAmount); winLossDisabledReason = `Record Win/Loss with Custom Amount: ${buttonAmountText}`; mugDisabledReason = `Record MUG loss of ${buttonAmountText}`; } else { winLossDisabledReason = "Enter a positive Custom Amount"; mugDisabledReason = "Enter a positive Custom Amount to record as a loss"; } } const winButton = container.querySelector('#rrWinButton'); const lossButton = container.querySelector('#rrLossButton'); const mugButton = container.querySelector('#rrMugButton'); winButton.disabled = !isCurrentAmountValid; lossButton.disabled = !isCurrentAmountValid; mugButton.disabled = state.selectedAmountMode !== 'custom' || !isCurrentAmountValid; winButton.title = winLossDisabledReason; lossButton.title = winLossDisabledReason; mugButton.title = (state.selectedAmountMode === 'custom') ? mugDisabledReason : "Mug requires Custom mode"; winButton.querySelector('span').textContent = isCurrentAmountValid ? ` ${buttonAmountText}` : ''; lossButton.querySelector('span').textContent = isCurrentAmountValid ? ` ${buttonAmountText}` : ''; mugButton.querySelector('span').textContent = (state.selectedAmountMode === 'custom' && isCurrentAmountValid) ? ` ${buttonAmountText}` : ''; // Fill Button const fillButton = container.querySelector('#rrFillBetButton'); let isNextBetFiniteAndPositive = isFinite(state.nextBet) && state.nextBet > 0; fillButton.disabled = !isNextBetFiniteAndPositive; fillButton.title = isNextBetFiniteAndPositive ? `Fill Torn input with Recommended Bet: ${formatMoney(state.nextBet)}` : "Next recommended bet is invalid or zero"; // Settings const settingsSection = container.querySelector('#rrSettingsSection'); settingsSection.classList.toggle('rr-settings-hidden', state.isSettingsCollapsed); if (!state.isSettingsCollapsed) { const bankrollInputEl = container.querySelector('#rrBankrollInput'); if (bankrollInputEl && document.activeElement !== bankrollInputEl) { bankrollInputEl.value = state.bankroll.toLocaleString(); } container.querySelector('#rrEffectiveBankrollSettingsDisplay').textContent = formatMoney(Number(state.bankroll) + Number(state.sessionProfitLoss)); container.querySelector('#rrRiskLevelSelect').value = state.riskLevel; container.querySelector('#rrStrategySelect').value = state.strategy; container.querySelector('#rrRoundingLevelSelect').value = state.roundingLevel; container.querySelector('#rrDisableAutofillCheckbox').checked = state.disableAutofill; } // History updateHistoryUI(); const removeBtn = container.querySelector('#rrRemoveLastEntryButton'); if (removeBtn) removeBtn.disabled = state.gameHistory.length === 0; if (removeBtn) removeBtn.title = state.gameHistory.length > 0 ? "Remove last entry" : "No history entries to remove"; } catch (uiError) { error("Error during UI update", uiError); const errorDiv = document.getElementById('rrErrorDisplay'); if (errorDiv) { errorDiv.textContent = "Critical UI Error! Check console."; errorDiv.className = 'rr-error-display error visible persistent'; } } } // --- Game Result Processing --- function handleGameResult(outcome, amount) { if (!state.initialized) { error("State not initialized."); return; } const currentPL = Number(state.sessionProfitLoss); const numericAmount = Number(amount); if (!outcome || !isFinite(numericAmount)) { error(`Invalid data. Outcome: ${outcome}, Amount: ${numericAmount}.`); return; } let profitLossChange = 0; let affectStreak = false; let amountForHistory = Math.abs(numericAmount); if (outcome === 'win') { if (numericAmount <= 0) { error("Win amount must be positive."); return; } profitLossChange = numericAmount; affectStreak = true; amountForHistory = numericAmount; } else if (outcome === 'loss') { if (numericAmount <= 0) { error("Loss amount must be positive."); return; } profitLossChange = -numericAmount; affectStreak = true; amountForHistory = numericAmount; } else if (outcome === 'mug') { if (numericAmount >= 0) { error("Mug amount should be negative internally."); return; } profitLossChange = numericAmount; affectStreak = false; amountForHistory = numericAmount; } else { error("Unknown outcome type:", outcome); return; } const newPL = currentPL + profitLossChange; log(`Handling result: ${outcome.toUpperCase()} | Amount: ${formatMoney(amountForHistory)} | P/L Change: ${formatPlAmount(profitLossChange)} | Old P/L: ${formatPlAmount(currentPL)} | New P/L: ${formatPlAmount(newPL)} | Affect Streak: ${affectStreak}`); if (!isFinite(newPL)) { error("P/L calc resulted in non-finite number.", { currentPL, profitLossChange }); return; } addGameToHistory(outcome, amountForHistory, currentPL); state.sessionProfitLoss = newPL; if (affectStreak) { const oldStreak = state.streak; if (outcome === 'win') state.streak = (state.streak <= 0) ? 1 : state.streak + 1; else state.streak = (state.streak >= 0) ? -1 : state.streak - 1; log(`Streak updated from ${oldStreak} to: ${state.streak}`); } else log(`Streak remains: ${state.streak}`); const plDisplay = document.getElementById('rrSessionPLDisplay'); if (plDisplay) { const flashClass = profitLossChange > 0 ? 'flash-positive' : 'flash-negative'; const outerSpan = plDisplay.closest('.rr-pl-display'); if(outerSpan) { outerSpan.classList.add(flashClass); setTimeout(() => outerSpan.classList.remove(flashClass), ACTION_FEEDBACK_DURATION_MS); } } calculateBets(); saveState(); updateUI(); } // --- View Change Observer --- function observeViewChanges() { log("Setting up View Change observer..."); if (viewChangeObserver) { viewChangeObserver.disconnect(); log("Previous View observer disconnected."); viewChangeObserver = null; } const reactRoot = document.getElementById(REACT_ROOT_ID); const targetNode = reactRoot?.parentElement || document.querySelector(CONTENT_WRAPPER_SELECTOR) || document.body; if (!targetNode) { error("Cannot observe view changes: Suitable target node not found."); return; } const observerCallback = (mutationsList, observer) => { let relevantChangeDetected = false; for (let mutation of mutationsList) { if (mutation.type === 'childList') { if (mutation.target === targetNode || mutation.target === reactRoot) { const addedNodes = Array.from(mutation.addedNodes); const removedNodes = Array.from(mutation.removedNodes); if (!addedNodes.every(n => n.id === UI_CONTAINER_ID) || !removedNodes.every(n => n.id === UI_CONTAINER_ID)) { relevantChangeDetected = true; break; } } } } if (relevantChangeDetected) { log("Relevant view change detected, queueing UI check..."); clearTimeout(viewChangeTimeout); viewChangeTimeout = setTimeout(() => { log("Running UI/Observer check after view change debounce."); ensureHelperUIVisible(); attachMoneyButtonListener(); }, VIEW_CHANGE_DEBOUNCE_MS); } }; viewChangeObserver = new MutationObserver(observerCallback); const config = { childList: true, subtree: true }; try { viewChangeObserver.observe(targetNode, config); log("View change observer started on:", targetNode); } catch (e) { error("Failed to start view change observer", e, targetNode); } } // --- CSV Export --- function escapeCsvValue(value) { if (value === null || value === undefined) return '""'; const stringValue = String(value); if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('"')) { return `"${stringValue.replace(/"/g, '""')}"`; } return `"${stringValue}"`; } function exportDataToCSV() { log("Exporting data to CSV..."); clearError(); try { const effectiveBankroll = Number(state.bankroll) + Number(state.sessionProfitLoss); const timestamp = new Date(); const dateStr = timestamp.toISOString().split('T')[0]; const timeStr = timestamp.toLocaleTimeString().replace(/:/g, '-'); const filename = `RR_Helper_Export_${dateStr}_${timeStr}.csv`; let csvContent = "Category,Item,Value\r\n"; csvContent += `Summary,Export Timestamp,"${timestamp.toISOString()}"\r\n`; csvContent += `Summary,Current Bankroll,${escapeCsvValue(state.bankroll)}\r\n`; csvContent += `Summary,Session P/L,${escapeCsvValue(state.sessionProfitLoss)}\r\n`; csvContent += `Summary,Effective Bankroll,${escapeCsvValue(effectiveBankroll)}\r\n`; csvContent += `Summary,Current Streak,${escapeCsvValue(state.streak)}\r\n`; csvContent += `Summary,Calculated Base Bet,${escapeCsvValue(state.baseBet)}\r\n`; csvContent += `Summary,Calculated Next Bet,${escapeCsvValue(isFinite(state.nextBet) ? state.nextBet : 'Infinity')}\r\n`; csvContent += `Summary,Next Bet Affordable?,${escapeCsvValue(state.isAffordable)}\r\n`; csvContent += `Settings,Strategy,${escapeCsvValue(STRATEGIES[state.strategy] || state.strategy)}\r\n`; csvContent += `Settings,Risk Level (Losses),${escapeCsvValue(state.riskLevel)}\r\n`; csvContent += `Settings,Rounding,${escapeCsvValue(ROUNDING_LEVELS[state.roundingLevel] || state.roundingLevel)}\r\n`; csvContent += `Settings,Autofill Disabled?,${escapeCsvValue(state.disableAutofill)}\r\n`; csvContent += "\r\nHistory Timestamp,Outcome,Amount,P/L Before Game\r\n"; [...state.gameHistory].reverse().forEach(game => { const gameTime = new Date(game.timestamp).toISOString(); csvContent += `${escapeCsvValue(gameTime)},${escapeCsvValue(game.outcome.toUpperCase())},${escapeCsvValue(game.bet)},${escapeCsvValue(game.plBefore)}\r\n`; }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); log("CSV export download triggered."); displayError("Exported data to CSV.", true); } else { error("CSV export failed: Browser does not support download attribute."); } } catch(e) { error("Error during CSV export process", e); } } // --- Add CSS Styles --- function addStyles() { log("Adding CSS styles..."); GM_addStyle(` /* Base Reset & Font */ #${UI_CONTAINER_ID} { all: initial !important; font: 13px/1.4 'Segoe UI', Arial, sans-serif !important; } #${UI_CONTAINER_ID} *, #${UI_CONTAINER_ID} *:before, #${UI_CONTAINER_ID} *:after { all: unset; box-sizing: border-box; font: inherit; color: inherit; line-height: inherit; } /* Main Container */ .rr-helper { display: block !important; background: #333; color: #f0f0f0; padding: 10px 15px; border-radius: 5px; margin: 15px 0; border: 1px solid #444; box-shadow: 0 1px 3px rgba(0,0,0,0.5); font-size: 13px; line-height: 1.4; } /* Top Bar */ .rr-top-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #444; font-size: 0.9em; position: relative; } .rr-bankroll-display { color: #ccc; } #rrEffectiveBankrollDisplay { color: #aaa; font-size: 12px; margin-left: 3px; } .rr-settings-button { background: none; border: 1px solid #777; color: #ccc; padding: 2px 6px; cursor: pointer; border-radius: 3px; font-size: 12px; } .rr-settings-button:hover { background-color: #444; } /* Error Display */ .rr-error-display { position: absolute; top: 100%; /* Position below top bar */ left: 50%; transform: translateX(-50%); background-color: rgba(211, 47, 47, 0.9); color: white; padding: 4px 10px; border-radius: 0 0 4px 4px; font-size: 12px; font-weight: bold; text-align: center; opacity: 0; transition: opacity 0.3s ease-in-out, top 0.3s ease-in-out; visibility: hidden; z-index: 100; box-shadow: 0 2px 5px rgba(0,0,0,0.3); white-space: nowrap; pointer-events: none; margin-top: 1px; /* Small gap */ max-width: 90%; } .rr-error-display.warning { background-color: rgba(255, 152, 0, 0.9); color: black; } .rr-error-display.visible { top: calc(100% + 1px); opacity: 1; visibility: visible; } .rr-error-display.persistent { /* Can add persistent styles if needed */ } /* Next Bet Area */ .rr-next-bet-area { background-color: #282828; padding: 10px; border-radius: 4px; margin-bottom: 10px; border: 1px solid #444; } .rr-next-bet-label { font-size: 11px; color: #aaa; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; text-transform: uppercase; letter-spacing: 0.5px; } .rr-streak-display { font-weight: bold; margin: 0 10px; } .rr-pl-display { font-size: 11px; font-weight: bold; transition: background-color 0.1s ease-out; border-radius: 3px;} .rr-pl-display.positive span { color: #4CAF50; } .rr-pl-display.negative span { color: #F44336; } .flash-positive { background-color: rgba(76, 175, 80, 0.3); } .flash-negative { background-color: rgba(211, 47, 47, 0.3); } .rr-next-bet-value-wrapper { display: flex; align-items: center; gap: 10px; justify-content: center; } .rr-next-bet-value { font-size: 22px; font-weight: bold; color: #eee; flex-grow: 1; text-align: center; cursor: help; padding: 2px 4px; border-radius: 3px; transition: background-color 0.2s; border: 1px solid transparent;} .rr-affordable-indicator { color: #ffcc80; font-size: 11px; margin-left: 5px; font-style: italic; cursor: help; display: inline-block;} .rr-fill-button { background-color: #4CAF50 !important; color: white !important; border: none !important; padding: 7px 14px !important; text-align: center; display: inline-block; font-size: 13px !important; cursor: pointer; border-radius: 3px; font-weight: bold; flex-shrink: 0;} .rr-fill-button:disabled { background-color: #555 !important; cursor: not-allowed; opacity: 0.7; } .rr-fill-button:hover:not(:disabled) { background-color: #45a049 !important; } /* Outcome Controls */ .rr-outcome-controls { display: flex; flex-direction: column; gap: 10px; background-color: #2f2f2f; padding: 10px; border-radius: 4px; margin-bottom: 10px; border: 1px solid #4a4a4a;} .rr-amount-mode { display: flex; flex-wrap: wrap; gap: 8px 15px; align-items: center; } .rr-radio-label { cursor: pointer; display: inline-flex; align-items: center; gap: 5px; font-size: 12px; padding: 3px 6px; border: 1px solid transparent; border-radius: 4px; transition: background-color 0.2s, border-color 0.2s; color: #ccc; } .rr-radio-label.rr-mode-active { background-color: #4a4a4a; border-color: #666; font-weight: bold; } .rr-radio-label input[type="radio"] { margin-right: 3px; } .rr-radio-label input[type="radio"]:disabled + span { opacity: 0.6; cursor: not-allowed; color: #888; } .rr-auto-pot-value { font-weight: bold; color: #64b5f6; margin-left: 2px; padding: 1px 3px; border-radius: 3px; transition: background-color 0.2s; border: 1px solid transparent; } .rr-pot-status { font-size: 10px; color: #aaa; margin-left: 4px; font-style: italic; } .rr-pot-status.status-watching { color: lightblue; } .rr-pot-status.status-error { color: salmon; } .rr-pot-status.status-not-found { color: #aaa; } .rr-custom-amount-input { padding: 5px 7px; background-color: #444; color: #eee; border: 1px solid #666; border-radius: 3px; font-size: 12px; width: 110px; transition: background-color 0.2s, border-color 0.2s; border: 1px solid transparent;} .rr-custom-amount-input:disabled { background-color: #3a3a3a; cursor: not-allowed; opacity: 0.6; } .rr-custom-amount-input::-webkit-inner-spin-button, .rr-custom-amount-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } .rr-custom-amount-input { -moz-appearance: textfield; } .rr-active-source { background-color: #555 !important; border-color: #777 !important; } /* Highlight */ .rr-action-buttons { display: flex; gap: 8px; justify-content: space-around; } .rr-outcome-button { flex-grow: 1; padding: 8px 10px !important; border: none; border-radius: 3px; font-weight: bold; cursor: pointer; font-size: 13px !important; transition: transform 0.1s ease-out, background-color 0.1s; display: flex; justify-content: center; align-items: center; } .rr-outcome-button span { font-size: 11px; margin-left: 5px; color: rgba(255,255,255,0.8); font-weight: normal; } .rr-outcome-button:disabled { background-color: #555 !important; color: #888 !important; cursor: not-allowed; opacity: 0.7; } .rr-outcome-button:disabled span { color: rgba(136,136,136,0.8); } .rr-win-button { background-color: #4CAF50 !important; color: white !important; } .rr-win-button:hover:not(:disabled) { background-color: #45a049 !important; } .rr-win-button span { color: rgba(0,0,0,0.6); } .rr-loss-button { background-color: #F44336 !important; color: white !important; } .rr-loss-button:hover:not(:disabled) { background-color: #e53935 !important; } .rr-loss-button span { color: rgba(0,0,0,0.6); } .rr-mug-button { background-color: #ff9800 !important; color: black !important; } .rr-mug-button:hover:not(:disabled) { background-color: #fb8c00 !important; } .rr-mug-button span { color: rgba(0,0,0,0.6); } #rrMugButton:disabled[title="Mug requires Custom mode"], #rrMugButton:disabled[title="Mug unavailable in Auto Pot mode"], #rrMugButton:disabled[title="Mug unavailable in Recommended Bet mode"] { background-color: #4a4a4a !important; border-color: #555 !important; opacity: 0.5; } .rr-outcome-button.rr-button-clicked { transform: scale(0.97); filter: brightness(1.1); } /* Settings Section */ .rr-settings-section { border-top: 1px solid #444; padding-top: 12px; margin-top: 8px; overflow: hidden; transition: max-height 0.3s ease-out, opacity 0.3s ease-out, border-top 0.3s ease-out, padding-top 0.3s ease-out, margin-top 0.3s ease-out; max-height: 500px; opacity: 1; } .rr-settings-hidden { max-height: 0; opacity: 0; border-top: none; padding-top: 0; margin-top: 0; overflow: hidden; } .rr-settings-section h5 { margin: 0 0 12px 0; font-size: 14px; color: #ccc; text-align: center; font-weight: bold; } .rr-settings-grid { display: grid; grid-template-columns: auto 1fr; gap: 10px 8px; align-items: center; } .rr-settings-grid label { text-align: right; font-size: 12px; color: #aaa; padding-right: 5px; } .rr-settings-grid input[type=text], .rr-settings-grid input[type=number], .rr-settings-grid select { padding: 5px 7px; background-color: #444; color: #eee; border: 1px solid #666; border-radius: 3px; font-size: 12px; width: 100%; max-width: 220px; justify-self: start; } .rr-calculated-bankroll { background-color: transparent; border: none; font-weight: bold; color: #eee; justify-self: start; padding: 5px 0; font-size: 12px; } .rr-settings-grid label[for="rrDisableAutofillCheckbox"] { text-align: left; grid-column: 1 / -1; justify-self: start; display: flex; align-items: center; gap: 5px; cursor: pointer; } #rrDisableAutofillCheckbox { width: auto; margin: 0; cursor: pointer; } .rr-settings-buttons { grid-column: 1 / -1; display: flex; gap: 10px; justify-content: flex-start; flex-wrap: wrap; margin-top: 10px; } .rr-action-button { padding: 5px 10px !important; font-size: 12px !important; margin-top: 0 !important; width: auto !important; flex-grow: 1; } .torn-btn.alternate.rr-action-button { background-color: #555; color: #ccc; border: 1px solid #777; } .torn-btn.alternate.rr-action-button:hover { background-color: #666; border-color: #888; color: #fff; } /* History Section */ .rr-history-section { border-top: 1px solid #444; padding-top: 12px; margin-top: 8px; } .rr-history-section h5 { margin: 0 0 8px 0; display: flex; justify-content: space-between; align-items: center; font-size: 14px; color: #ccc; font-weight: bold; } #rrHistoryList { list-style: none; padding: 0; margin: 0; max-height: 160px; overflow-y: auto; font-size: 11px; background-color: #2a2a2a; border: 1px solid #444; border-radius: 3px; margin-bottom: 8px; } #rrHistoryList li { padding: 4px 8px; border-bottom: 1px solid #383838; cursor: default; } #rrHistoryList li:last-child { border-bottom: none; } .rr-history-win { color: #81C784; } .rr-history-loss { color: #E57373; } .rr-history-mug { color: #FFB74D; font-style: italic; } .rr-remove-button { background: none; border: none; color: #aaa; cursor: pointer; font-size: 16px; padding: 0 5px; } .rr-remove-button:disabled { color: #666; cursor: not-allowed; } .rr-remove-button:hover:not(:disabled) { color: #eee; } #rrHistoryList::-webkit-scrollbar { width: 6px !important; height: 6px !important; } #rrHistoryList::-webkit-scrollbar-thumb { background:#555 !important; border-radius:3px !important; } #rrHistoryList::-webkit-scrollbar-track { background: #2a2a2a; } /* General Helpers */ .positive { color: lightgreen !important; } .negative { color: salmon !important; } .rr-streak-win { color: lightgreen !important; } .rr-streak-loss { color: salmon !important; } .rr-streak-neutral { color: #ccc !important; } .rr-affordable-indicator { display: inline-block; } .rr-next-bet-value.rr-max-streak { color: orange !important; font-style: italic;} .rr-divider { border: none; border-top: 1px dashed #555; margin: 8px 0; } `); // Use optional chaining for GM_info access log(`Initializing RR Helper v${GM_info?.script?.version || '?.?.?'}...`); // This log is disabled but kept for structure loadState(); state.initialized = true; log("State initialization complete."); // Disabled calculateBets(); saveState(); // Save potentially updated state after initial load/calc setTimeout(ensureHelperUIVisible, 250); // Initial UI injection/check observeViewChanges(); // Start observing DOM changes // Listener for tab focus document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { log("Tab focused, ensuring UI & checking autofill..."); // Disabled setTimeout(ensureHelperUIVisible, 50); // Quick check setTimeout(() => updateBetInput(false), 250); // Check bet input after potential UI updates } }); log("Focus listener added."); // Disabled // Start autofill interval check if (autofillIntervalId) clearInterval(autofillIntervalId); autofillIntervalId = setInterval(() => { if (document.visibilityState === 'visible') { updateBetInput(false); // Check if autofill is needed } }, AUTOFILL_CHECK_INTERVAL); log(`Interval check (${AUTOFILL_CHECK_INTERVAL / 1000}s) for autofill started.`); // Disabled log("RR Helper Initialization Finished."); // Disabled } /* ------------------------------------------------------------- * Initialization Bootstrap Wrapper * ----------------------------------------------------------- */ function initialize() { // single entry-point: inject CSS + run all startup logic addStyles(); } // --- Initialization Wait Loop --- const maxInitAttempts = 20; let initAttempts = 0; const initInterval = setInterval(() => { initAttempts++; // Check if the react root element is available if (document.getElementById(REACT_ROOT_ID)) { clearInterval(initInterval); log(`#react-root found after ${initAttempts} attempts. Initializing...`); // Disabled initialize(); // Call the bootstrap wrapper } else if (initAttempts >= maxInitAttempts) { clearInterval(initInterval); error(`Failed to find #react-root after ${maxInitAttempts} attempts. Attempting initialization anyway.`); initialize(); // Try to initialize even if root isn't found } else { log(`Waiting for #react-root... (Attempt ${initAttempts}/${maxInitAttempts})`); // Disabled } }, 500); })();