RR Strategy Helper (Beta Test)

RR Helper Script for various strategies

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();