您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
RR tracker with cool features, now with an integrated stats panel and profit graph, Martingale bet auto-start, a powerful dynamic bet calculator, and a fully scalable UI.
// ==UserScript== // @name RR Tracker v10 (Graphing & Stats API supported) // @namespace https://greasyfork.org/users/1493252 // @version 10 // @description RR tracker with cool features, now with an integrated stats panel and profit graph, Martingale bet auto-start, a powerful dynamic bet calculator, and a fully scalable UI. //###Your Torn RR Tracker: Quick Guide //###Auto-Tracking //###Just open Russian Roulette, and your tracker starts automatically. It records every win and loss, showing your profit, win rate, and streaks. //###Panel Controls //###* Drag: Click and drag the ☰ icon to move the panel anywhere you like. //###* Shift + Drag: Hold Shift and drag with your mouse from anywhere on the panel to move it. //###* Collapse/Expand: Click the ► (or ▪) icon to shrink or expand the panel. //###* Hide/Show: If Auto-Hide is on in settings, the tracker hides when you leave RR and reappears when you return. //###Settings (⚙️ icon) //###* Panel Size: Use the slider to zoom the entire panel in or out for a perfect fit. //###* Panel Opacity: Adjust how see-through the panel is. //###* Profit/Loss Alerts: Set targets to be notified when you hit a certain profit or loss. //###* Mini-Bar Count: Choose how many recent games show in the collapsed view. //###* Mini-Button Size: Adjust the size of the bet buttons in the collapsed view. //###* Bet Button Count: Use a slider to choose how many bet buttons to display (4-16). //###* Reset Data: Clear all your tracked stats and profit. //###* Edit Bets: Customize your bet amounts in a dedicated panel. //###* Charging Shoots: Toggle the auto-shoot helper panel on or off. //###Bets (💰 icon) //###* Smart Bets: The panel now shows your recent match history and allows you to auto-start bets. //###Stats (📊 icon) (NEW!) //###* Open a new panel with a cumulative profit graph. //###* Select timeframes: 7 Days, 1 Month, 3 Months, 6 Months, or 1 Year. //###* See key stats for the selected period like Total Profit, Win/Loss, and Games Played. //###Edit Bets (✏️ NEW LOGIC!) //###* Enable "Dynamic Bets" to use the new calculator. //###* Set your strategy (Capital, Streak, Multiplier). //###* Use the lock icons (🔒/🔓) to choose your calculation method. //###* Lock 'Gambling Capital' to calculate the 'Starting Bet'. //###* Lock 'Starting Bet' to calculate the required 'Gambling Capital'. //###* The script populates your bet buttons automatically. //###* Disable "Dynamic Bets" to return to setting bets manually. //###Charging Shoots (⚡) //###* When enabled in settings, a new panel appears during matches. //###* Click "+ Add Charge" to load up to 3 shoots. //###* The script will automatically click the "Shoot" button for you. // @match https://www.torn.com/page.php?sid=russianRoulette* // @grant GM.xmlHttpRequest // @connect api.torn.com // @run-at document-idle // @license MIT // ==/UserScript== (function waitUntilReady() { 'use strict'; // --- Constants --- const PANEL_ID = 'rr-tracker-panel'; const STORAGE = 'torn_rr_tracker_results'; const PROFIT_STORAGE = 'torn_rr_total_profit'; const API_KEY_STORAGE = 'rr_tracker_api_key'; const LAST_SYNC_KEY = 'rr_tracker_last_sync'; const POS_KEY = 'rr_panelPos'; const COLLAPSE_KEY = 'rr_panelCollapsed'; const AUTOHIDE_KEY = 'rr_autoHide'; const MAX_DISPLAY_KEY = 'rr_maxDisplayMatches'; const OPACITY_KEY = 'rr_panelOpacity'; const PROFIT_TARGET_KEY = 'rr_profitTarget'; const LOSS_LIMIT_KEY = 'rr_lossLimit'; const ALERT_SHOWN_PROFIT_KEY = 'rr_alertShownProfit'; const ALERT_SHOWN_LOSS_KEY = 'rr_alertShownLoss'; const MINI_BAR_COUNT_KEY = 'rr_miniBarCount'; const MINI_BUTTON_SIZE_KEY = 'rr_miniButtonSize'; const MANUAL_BETS_STORAGE_KEY = 'RRManualBets_v3'; const DEFAULT_MANUAL_BETS = [10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000, 10240000]; const CHARGED_SHOOTS_PANEL_ID = 'rr-charged-shoots-panel'; const CHARGED_SHOOTS_ENABLED_KEY = 'rr_chargedshootsEnabled'; const CHARGED_SHOOTS_POS_KEY = 'rr_chargedshootsPanelPos'; const CHARGED_SHOOTS_COUNT_KEY = 'rr_chargedShootsCount'; const DYNAMIC_BETS_ENABLED_KEY = 'rr_dynamicBetsEnabled'; const DYNAMIC_BETS_SETTINGS_KEY = 'rr_dynamicBetsSettings_v2'; const UI_BUTTON_COUNT_KEY = 'rr_uiButtonCount'; const MAX_L_STREAK_CAP = 15; const PANEL_SCALE_KEY = 'rr_panelScale'; const DEFAULT_DYNAMIC_SETTINGS = { testBet: 1000, capital: 100000000, maxLStreak: 10, martingaleValue: 2, reinforce: false, lockedField: 'capital', startingBet: 0 }; // --- State Variables --- let apiKey = ''; let lastSyncTime = 0; // --- MODIFIED: Added showStatsPanel --- let manualBets, currentBets, showBetsPanel, showEditBetsPanel, showStatsPanel, isDragging, dragMouseX, dragMouseY, isTwoFingerDragging, initialTouchMidX, initialTouchMidY, initialPanelX, initialPanelY; let isChargedshootsEnabled, chargedshoots, chargedshootsPanel, chargedshootsDisplay, isChargedPanelDragging, dragChargedPanelMouseX, dragChargedPanelMouseY, isChargedPanelTwoFingerDragging, initialChargedPanelTouchMidX, initialChargedPanelTouchMidY, initialChargedPanelX, initialChargedPanelY; let results, totalProfit, collapsed, autoHide, showSettings, maxDisplayMatches, currentOpacity, profitTarget, lossLimit, alertShownProfit, alertShownLoss, miniBarCount, miniButtonSize; let isShootDelayed = false; let uiButtonCount; let manualEditInputs = []; let panelScale; // --- Dynamic Bets State --- let dynamicBetsEnabled, dynamicBetsSettings; let capitalInput, startingBetInput, testBetInput, capitalLock, startingBetLock; // --- NEW: Stats Panel State --- let currentStatsTimeframe = 7; // Default to 7 days const nativeSet = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; // --- Initial Checks --- if (document.getElementById(PANEL_ID)) return; if (!document.body.innerText.includes('Password') && !document.body.innerText.includes('POT MONEY') && !document.body.innerText.includes('Shoot') && !document.body.innerText.includes('Players') && !document.querySelector('.create-game-section') && !document.querySelector('.russian-roulette-container')) { return setTimeout(waitUntilReady, 200); } // --- Load Data and Initialize Variables --- function initializeState() { apiKey = localStorage.getItem(API_KEY_STORAGE) || '';lastSyncTime = parseInt(localStorage.getItem(LAST_SYNC_KEY) || '0', 10); // --- MODIFIED: Initialized showStatsPanel --- manualBets = []; currentBets = []; showBetsPanel = false; showEditBetsPanel = false; showStatsPanel = false; isDragging = false; dragMouseX = 0; dragMouseY = 0; isTwoFingerDragging = false; initialTouchMidX = 0; initialTouchMidY = 0; initialPanelX = 0; initialPanelY = 0; chargedshoots = parseInt(localStorage.getItem(CHARGED_SHOOTS_COUNT_KEY) || '0', 10); isChargedPanelDragging = false; dragChargedPanelMouseX = 0; dragChargedPanelMouseY = 0; isChargedPanelTwoFingerDragging = false; results = JSON.parse(localStorage.getItem(STORAGE) || '[]'); totalProfit = parseFloat(localStorage.getItem(PROFIT_STORAGE) || '0'); collapsed = JSON.parse(localStorage.getItem(COLLAPSE_KEY) || 'false'); autoHide = JSON.parse(localStorage.getItem(AUTOHIDE_KEY) || 'false'); showSettings = false; maxDisplayMatches = parseInt(localStorage.getItem(MAX_DISPLAY_KEY) || '100', 10); if (isNaN(maxDisplayMatches) || maxDisplayMatches < 1) { maxDisplayMatches = 100; localStorage.setItem(MAX_DISPLAY_KEY, maxDisplayMatches.toString()); } currentOpacity = parseFloat(localStorage.getItem(OPACITY_KEY) || '0.6'); if (isNaN(currentOpacity) || currentOpacity < 0.1 || currentOpacity > 1.0) { currentOpacity = 0.6; localStorage.setItem(OPACITY_KEY, currentOpacity.toString()); } profitTarget = parseFloat(localStorage.getItem(PROFIT_TARGET_KEY) || '0'); lossLimit = parseFloat(localStorage.getItem(LOSS_LIMIT_KEY) || '0'); alertShownProfit = JSON.parse(localStorage.getItem(ALERT_SHOWN_PROFIT_KEY) || 'false'); alertShownLoss = JSON.parse(localStorage.getItem(ALERT_SHOWN_LOSS_KEY) || 'false'); miniBarCount = parseInt(localStorage.getItem(MINI_BAR_COUNT_KEY) || '10', 10); if (isNaN(miniBarCount) || miniBarCount < 1 || miniBarCount > 50) { miniBarCount = 10; localStorage.setItem(MINI_BAR_COUNT_KEY, miniBarCount.toString()); } miniButtonSize = parseInt(localStorage.getItem(MINI_BUTTON_SIZE_KEY) || '9', 10); if (isNaN(miniButtonSize) || miniButtonSize < 7 || miniButtonSize > 14) { miniButtonSize = 9; localStorage.setItem(MINI_BUTTON_SIZE_KEY, miniButtonSize.toString()); } isChargedshootsEnabled = JSON.parse(localStorage.getItem(CHARGED_SHOOTS_ENABLED_KEY) || 'false'); uiButtonCount = parseInt(localStorage.getItem(UI_BUTTON_COUNT_KEY) || '11', 10); if (isNaN(uiButtonCount) || uiButtonCount < 4 || uiButtonCount > 16) { uiButtonCount = 11; localStorage.setItem(UI_BUTTON_COUNT_KEY, uiButtonCount.toString()); } panelScale = parseFloat(localStorage.getItem(PANEL_SCALE_KEY) || '1.0'); if (isNaN(panelScale) || panelScale < 0.7 || panelScale > 1.5) { panelScale = 1.0; localStorage.setItem(PANEL_SCALE_KEY, panelScale.toString()); } dynamicBetsEnabled = JSON.parse(localStorage.getItem(DYNAMIC_BETS_ENABLED_KEY) || 'false'); try { const storedDynamicSettings = JSON.parse(localStorage.getItem(DYNAMIC_BETS_SETTINGS_KEY)); dynamicBetsSettings = { ...DEFAULT_DYNAMIC_SETTINGS, ...storedDynamicSettings }; } catch { dynamicBetsSettings = { ...DEFAULT_DYNAMIC_SETTINGS }; } manualBets = loadManualBets(); adjustManualBetsArray(); } initializeState(); // --- UI Creation --- const panel = document.createElement('div'); panel.id = PANEL_ID; Object.assign(panel.style, { position: 'fixed', top: '12px', left: '12px', background: `rgba(0,0,0,${currentOpacity})`, color: '#fff', fontFamily: 'monospace', fontSize: '14px', padding: '36px 12px 12px', borderRadius: '10px', boxShadow: '0 0 12px rgba(255,0,0,0.3)', zIndex: '9999999', userSelect: 'none', display: 'flex', flexDirection: 'column', gap: '8px', minWidth: '140px', transformOrigin: 'top left', transform: `scale(${panelScale})` }); document.body.appendChild(panel); try { const pos = JSON.parse(localStorage.getItem(POS_KEY) || '{}'); if (pos.top && pos.left) { panel.style.top = pos.top; panel.style.left = pos.left; } } catch {} chargedshootsPanel = document.createElement('div'); chargedshootsPanel.id = CHARGED_SHOOTS_PANEL_ID; Object.assign(chargedshootsPanel.style, { position: 'fixed', top: '150px', left: '12px', background: `rgba(0,0,0,${currentOpacity})`, color: '#fff', fontFamily: 'monospace', fontSize: '14px', padding: '10px 12px', borderRadius: '10px', boxShadow: '0 0 12px rgba(255,0,0,0.3)', zIndex: '9999998', userSelect: 'none', display: 'none', flexDirection: 'column', alignItems: 'center', gap: '8px', touchAction: 'none', transformOrigin: 'top left', transform: `scale(${panelScale})` }); document.body.appendChild(chargedshootsPanel); try { const pos = JSON.parse(localStorage.getItem(CHARGED_SHOOTS_POS_KEY) || '{}'); if (pos.top && pos.left) { chargedshootsPanel.style.top = pos.top; chargedshootsPanel.style.left = pos.left; } } catch {} const chargedshootsTitle = document.createElement('div'); chargedshootsTitle.textContent = 'Charging Shoots'; chargedshootsTitle.style.fontWeight = 'bold'; chargedshootsPanel.appendChild(chargedshootsTitle); chargedshootsDisplay = document.createElement('div'); chargedshootsDisplay.textContent = `Charges: ${chargedshoots} / 3`; chargedshootsDisplay.style.fontSize = '16px'; chargedshootsDisplay.title = "Shows how many auto-shoots are currently loaded. Resets after each match."; chargedshootsPanel.appendChild(chargedshootsDisplay); const addChargeBtn = document.createElement('button'); addChargeBtn.textContent = '+ Add Charge'; addChargeBtn.title = "Click to add a 'charge'. The script will automatically click the 'Shoot' button once per charge during a match."; Object.assign(addChargeBtn.style, { background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer', width: '100%' }); addChargeBtn.onmouseenter = () => addChargeBtn.style.background = 'rgba(255,255,255,0.2)'; addChargeBtn.onmouseleave = () => addChargeBtn.style.background = 'rgba(255,255,255,0.1)'; addChargeBtn.onclick = () => { if (chargedshoots < 3) { chargedshoots++; updateChargedshootsDisplay(); localStorage.setItem(CHARGED_SHOOTS_COUNT_KEY, chargedshoots.toString()); } }; chargedshootsPanel.appendChild(addChargeBtn); const alertMessageDiv = document.createElement('div'); alertMessageDiv.id = 'rr-alert-message'; Object.assign(alertMessageDiv.style, { display: 'none', padding: '8px', marginBottom: '8px', borderRadius: '6px', textAlign: 'center', fontWeight: 'bold', fontSize: '16px', color: 'white', cursor: 'pointer', border: '1px solid transparent', transition: 'background-color 0.3s, border-color 0.3s' }); panel.appendChild(alertMessageDiv); const miniBar = document.createElement('div'); Object.assign(miniBar.style, { display: 'none', flexDirection: 'column', gap: '2px', padding: '4px 0' }); panel.appendChild(miniBar); const profitMini = document.createElement('div'); Object.assign(profitMini.style, { display: 'none', fontSize: '14px', fontFamily: 'monospace', margin: '2px 0' }); panel.appendChild(profitMini); const statusDiv = document.createElement('div'); statusDiv.title = "Collapse/Expand the tracker panel."; Object.assign(statusDiv.style, { position: 'absolute', top: '8px', left: '8px', width: '20px', height: '20px', fontSize: '18px', cursor: 'pointer', color: 'rgba(255,255,255,0.7)' }); panel.appendChild(statusDiv); const dragHandle = document.createElement('div'); dragHandle.textContent = '☰'; dragHandle.title = "Click and drag to move the panel."; Object.assign(dragHandle.style, { position: 'absolute', top: '8px', right: '8px', width: '20px', height: '20px', fontSize: '18px', cursor: 'move', color: 'rgba(255,255,255,0.7)', touchAction: 'none' }); panel.appendChild(dragHandle); const statsGroup = document.createElement('div'); Object.assign(statsGroup.style, { display: 'flex', flexDirection: 'column', gap: '4px' }); panel.appendChild(statsGroup); const profitDiv = document.createElement('div'); profitDiv.title = "Total profit or loss tracked since the last reset."; const dailyProfitDiv = document.createElement('div'); dailyProfitDiv.title = "Profit or loss for today (since midnight local time)."; const winrateDiv = document.createElement('div'); const streakDiv = document.createElement('div'); statsGroup.append(profitDiv, dailyProfitDiv, winrateDiv, streakDiv); const resultsContainer = document.createElement('div'); Object.assign(resultsContainer.style, { maxHeight: '140px', overflowY: 'auto', marginTop: '4px' }); statsGroup.appendChild(resultsContainer); const settingsPanel = document.createElement('div'); Object.assign(settingsPanel.style, { display: 'none', flexDirection: 'column', gap: '8px', padding: '12px 0' }); panel.appendChild(settingsPanel); const settingsTitle = document.createElement('div'); settingsTitle.textContent = 'Settings'; Object.assign(settingsTitle.style, { fontSize: '16px', fontWeight: 'bold', marginBottom: '4px' }); settingsPanel.appendChild(settingsTitle); const backButtonSettings = document.createElement('button'); backButtonSettings.textContent = '← Back'; Object.assign(backButtonSettings.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer', marginBottom: '8px' }); backButtonSettings.onmouseenter = () => backButtonSettings.style.background = 'rgba(255,255,255,0.2)'; backButtonSettings.onmouseleave = () => backButtonSettings.style.background = 'rgba(255,255,255,0.1)'; backButtonSettings.onclick = () => { showSettings = false; showBetsPanel = false; showEditBetsPanel = false; refreshAll(); }; settingsPanel.appendChild(backButtonSettings); const settingsScrollContainer = document.createElement('div'); Object.assign(settingsScrollContainer.style, { display: 'flex', flexDirection: 'column', gap: '8px', maxHeight: '300px', overflowY: 'auto', paddingRight: '5px' }); settingsPanel.appendChild(settingsScrollContainer); const apiSettingDiv = document.createElement('div'); Object.assign(apiSettingDiv.style, { display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', border: '1px solid #444', borderRadius: '6px' }); const apiKeyLabel = document.createElement('label'); apiKeyLabel.textContent = 'Torn API Key (Auto-Syncs):'; apiKeyLabel.htmlFor = 'api-key-input'; const apiKeyInput = document.createElement('input'); apiKeyInput.type = 'password'; apiKeyInput.id = 'api-key-input'; apiKeyInput.value = apiKey; apiKeyInput.placeholder = 'Enter Full Access API Key'; apiKeyInput.title = 'Enter your Torn API key here. A limited-access key is recommended.'; Object.assign(apiKeyInput.style, { width: '95%', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); apiKeyInput.onchange = () => { apiKey = apiKeyInput.value.trim(); localStorage.setItem(API_KEY_STORAGE, apiKey); initiateSyncing(); }; apiSettingDiv.append(apiKeyLabel, apiKeyInput); settingsScrollContainer.appendChild(apiSettingDiv); const panelScaleSettingDiv = document.createElement('div'); Object.assign(panelScaleSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const panelScaleLabel = document.createElement('label'); panelScaleLabel.textContent = `Panel Size (${Math.round(panelScale * 100)}%):`; panelScaleLabel.htmlFor = 'panel-scale-slider'; Object.assign(panelScaleLabel.style, { flexShrink: '0' }); const panelScaleSlider = document.createElement('input'); panelScaleSlider.type = 'range'; panelScaleSlider.id = 'panel-scale-slider'; panelScaleSlider.min = '0.7'; panelScaleSlider.max = '1.5'; panelScaleSlider.step = '0.05'; panelScaleSlider.value = panelScale; panelScaleSlider.title = "Adjust the overall size (zoom) of the tracker panel."; Object.assign(panelScaleSlider.style, { width: '100px', cursor: 'pointer' }); panelScaleSlider.oninput = () => { panelScaleLabel.textContent = `Panel Size (${Math.round(parseFloat(panelScaleSlider.value) * 100)}%):`; }; panelScaleSlider.onchange = () => { panelScale = parseFloat(panelScaleSlider.value); panel.style.transform = `scale(${panelScale})`; chargedshootsPanel.style.transform = `scale(${panelScale})`; localStorage.setItem(PANEL_SCALE_KEY, panelScale.toString()); }; panelScaleSettingDiv.append(panelScaleLabel, panelScaleSlider); settingsScrollContainer.appendChild(panelScaleSettingDiv); // ... (rest of the settings UI elements from the original script are assumed here, no changes needed) ... // --- SKIPPING AHEAD TO THE NEW UI ELEMENT INSERTION POINT --- // (For brevity, I'm omitting the large block of settings code that doesn't change) const maxMatchesSettingDiv = document.createElement('div'); Object.assign(maxMatchesSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const maxMatchesLabel = document.createElement('label'); maxMatchesLabel.textContent = 'Max Matches Displayed:'; maxMatchesLabel.htmlFor = 'max-matches-input'; Object.assign(maxMatchesLabel.style, { flexShrink: '0' }); const maxMatchesInput = document.createElement('input'); maxMatchesInput.type = 'number'; maxMatchesInput.id = 'max-matches-input'; maxMatchesInput.min = '1'; maxMatchesInput.max = '100000'; maxMatchesInput.value = maxDisplayMatches; maxMatchesInput.title = "Set the maximum number of recent matches to show in the main panel."; Object.assign(maxMatchesInput.style, { width: '60px', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); maxMatchesInput.onchange = () => { let newValue = parseInt(maxMatchesInput.value, 10); if (isNaN(newValue) || newValue < 1) { newValue = 1; } if (newValue > 100000) newValue = 100000; maxDisplayMatches = newValue; maxMatchesInput.value = maxDisplayMatches; localStorage.setItem(MAX_DISPLAY_KEY, maxDisplayMatches.toString()); while (results.length > maxDisplayMatches) { results.pop(); } saveResults(); refreshAll(); }; maxMatchesSettingDiv.append(maxMatchesLabel, maxMatchesInput); settingsScrollContainer.appendChild(maxMatchesSettingDiv); const transparencySettingDiv = document.createElement('div'); Object.assign(transparencySettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const transparencyLabel = document.createElement('label'); transparencyLabel.textContent = 'Panel Opacity:'; transparencyLabel.htmlFor = 'transparency-slider'; Object.assign(transparencyLabel.style, { flexShrink: '0' }); const transparencySlider = document.createElement('input'); transparencySlider.type = 'range'; transparencySlider.id = 'transparency-slider'; transparencySlider.min = '0.1'; transparencySlider.max = '1.0'; transparencySlider.step = '0.05'; transparencySlider.value = currentOpacity; transparencySlider.title = "Adjust the transparency of the tracker panel."; Object.assign(transparencySlider.style, { width: '100px', padding: '4px', border: 'none', background: 'transparent', cursor: 'pointer' }); transparencySlider.oninput = () => { currentOpacity = parseFloat(transparencySlider.value); panel.style.background = `rgba(0,0,0,${currentOpacity})`; localStorage.setItem(OPACITY_KEY, currentOpacity.toString()); refreshAll(); }; transparencySettingDiv.append(transparencyLabel, transparencySlider); settingsScrollContainer.appendChild(transparencySettingDiv); const profitTargetSettingDiv = document.createElement('div'); Object.assign(profitTargetSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const profitTargetLabel = document.createElement('label'); profitTargetLabel.textContent = 'Profit Target ($):'; profitTargetLabel.htmlFor = 'profit-target-input'; Object.assign(profitTargetLabel.style, { flexShrink: '0' }); const profitTargetInput = document.createElement('input'); profitTargetInput.type = 'text'; profitTargetInput.id = 'profit-target-input'; profitTargetInput.value = profitTarget; profitTargetInput.title = "Set a profit target. You'll get a persistent alert when this target is reached."; Object.assign(profitTargetInput.style, { width: '80px', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); profitTargetInput.onchange = () => { let newValue = parseBetInput(profitTargetInput.value); if (newValue === null || newValue < 0) newValue = 0; profitTarget = newValue; profitTargetInput.value = formatNumberToKMB(profitTarget); localStorage.setItem(PROFIT_TARGET_KEY, profitTarget.toString()); alertShownProfit = false; localStorage.setItem(ALERT_SHOWN_PROFIT_KEY, 'false'); refreshAll(); }; profitTargetSettingDiv.append(profitTargetLabel, profitTargetInput); settingsScrollContainer.appendChild(profitTargetSettingDiv); const lossLimitSettingDiv = document.createElement('div'); Object.assign(lossLimitSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const lossLimitLabel = document.createElement('label'); lossLimitLabel.textContent = 'Loss Limit ($):'; lossLimitLabel.htmlFor = 'loss-limit-input'; Object.assign(lossLimitLabel.style, { flexShrink: '0' }); const lossLimitInput = document.createElement('input'); lossLimitInput.type = 'text'; lossLimitInput.id = 'loss-limit-input'; lossLimitInput.value = lossLimit; lossLimitInput.title = "Set a loss limit. You'll get a persistent alert if your losses reach this amount."; Object.assign(lossLimitInput.style, { width: '80px', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); lossLimitInput.onchange = () => { let newValue = parseBetInput(lossLimitInput.value); if (newValue === null || newValue < 0) newValue = 0; lossLimit = newValue; lossLimitInput.value = formatNumberToKMB(lossLimit); localStorage.setItem(LOSS_LIMIT_KEY, lossLimit.toString()); alertShownLoss = false; localStorage.setItem(ALERT_SHOWN_LOSS_KEY, 'false'); refreshAll(); }; lossLimitSettingDiv.append(lossLimitLabel, lossLimitInput); settingsScrollContainer.appendChild(lossLimitSettingDiv); const clearAlertsBtn = document.createElement('button'); clearAlertsBtn.textContent = '✔️ Clear Alerts'; clearAlertsBtn.title = "Dismiss any active Profit Target or Loss Limit alerts."; Object.assign(clearAlertsBtn.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); clearAlertsBtn.onmouseenter = () => clearAlertsBtn.style.background = 'rgba(255,255,255,0.2)'; clearAlertsBtn.onmouseleave = () => clearAlertsBtn.style.background = 'rgba(255,255,255,0.1)'; clearAlertsBtn.onclick = () => { alertShownProfit = false; alertShownLoss = false; localStorage.setItem(ALERT_SHOWN_PROFIT_KEY, 'false'); localStorage.setItem(ALERT_SHOWN_LOSS_KEY, 'false'); alertMessageDiv.style.display = 'none'; refreshAll(); }; settingsScrollContainer.appendChild(clearAlertsBtn); const miniBarCountSettingDiv = document.createElement('div'); Object.assign(miniBarCountSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const miniBarCountLabel = document.createElement('label'); miniBarCountLabel.textContent = 'Mini-Bar Count:'; miniBarCountLabel.htmlFor = 'mini-bar-count-input'; Object.assign(miniBarCountLabel.style, { flexShrink: '0' }); const miniBarCountInput = document.createElement('input'); miniBarCountInput.type = 'number'; miniBarCountInput.id = 'mini-bar-count-input'; miniBarCountInput.min = '1'; miniBarCountInput.max = '50'; miniBarCountInput.value = miniBarCount; miniBarCountInput.title = "Set how many recent match indicators (W/L circles) are shown in the collapsed view."; Object.assign(miniBarCountInput.style, { width: '60px', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); miniBarCountInput.onchange = () => { let newValue = parseInt(miniBarCountInput.value, 10); if (isNaN(newValue) || newValue < 1) newValue = 1; if (newValue > 50) newValue = 50; miniBarCount = newValue; miniBarCountInput.value = miniBarCount; localStorage.setItem(MINI_BAR_COUNT_KEY, miniBarCount.toString()); refreshAll(); }; miniBarCountSettingDiv.append(miniBarCountLabel, miniBarCountInput); settingsScrollContainer.appendChild(miniBarCountSettingDiv); const miniButtonSizeSettingDiv = document.createElement('div'); Object.assign(miniButtonSizeSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const miniButtonSizeLabel = document.createElement('label'); miniButtonSizeLabel.textContent = 'Mini-Button Size:'; miniButtonSizeLabel.htmlFor = 'mini-button-size-slider'; Object.assign(miniButtonSizeLabel.style, { flexShrink: '0' }); const miniButtonSizeSlider = document.createElement('input'); miniButtonSizeSlider.type = 'range'; miniButtonSizeSlider.id = 'mini-button-size-slider'; miniButtonSizeSlider.min = '7'; miniButtonSizeSlider.max = '14'; miniButtonSizeSlider.step = '1'; miniButtonSizeSlider.value = miniButtonSize; miniButtonSizeSlider.title = "Adjust the font size of the bet buttons in the collapsed view."; Object.assign(miniButtonSizeSlider.style, { width: '100px', padding: '4px', border: 'none', background: 'transparent', cursor: 'pointer' }); miniButtonSizeSlider.oninput = () => { miniButtonSize = parseInt(miniButtonSizeSlider.value, 10); localStorage.setItem(MINI_BUTTON_SIZE_KEY, miniButtonSize.toString()); refreshAll(); }; miniButtonSizeSettingDiv.append(miniButtonSizeLabel, miniButtonSizeSlider); settingsScrollContainer.appendChild(miniButtonSizeSettingDiv); const buttonCountSettingDiv = document.createElement('div'); Object.assign(buttonCountSettingDiv.style, { display: 'flex', alignItems: 'center', gap: '8px' }); const buttonCountLabel = document.createElement('label'); buttonCountLabel.textContent = `Bet Buttons (${uiButtonCount}):`; buttonCountLabel.htmlFor = 'button-count-slider'; Object.assign(buttonCountLabel.style, { flexShrink: '0' }); const buttonCountSlider = document.createElement('input'); buttonCountSlider.type = 'range'; buttonCountSlider.id = 'button-count-slider'; buttonCountSlider.min = '4'; buttonCountSlider.max = '16'; buttonCountSlider.step = '1'; buttonCountSlider.value = uiButtonCount; buttonCountSlider.title = "Set how many bet buttons are available in the UI (4 to 16)."; Object.assign(buttonCountSlider.style, { width: '100px', padding: '4px', border: 'none', background: 'transparent', cursor: 'pointer' }); buttonCountSlider.oninput = () => { buttonCountLabel.textContent = `Bet Buttons (${buttonCountSlider.value}):`; }; buttonCountSlider.onchange = () => { uiButtonCount = parseInt(buttonCountSlider.value, 10); localStorage.setItem(UI_BUTTON_COUNT_KEY, uiButtonCount.toString()); adjustManualBetsArray(); buildManualBetInputs(); calculateBetsAndRefresh(); }; buttonCountSettingDiv.append(buttonCountLabel, buttonCountSlider); settingsScrollContainer.appendChild(buttonCountSettingDiv); const editBetsButton = document.createElement('button'); editBetsButton.textContent = '✏️ Edit Bets'; editBetsButton.title = "Open the panel to configure your manual or dynamic betting strategy."; Object.assign(editBetsButton.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); editBetsButton.onmouseenter = () => editBetsButton.style.background = 'rgba(255,255,255,0.2)'; editBetsButton.onmouseleave = () => editBetsButton.style.background = 'rgba(255,255,255,0.1)'; editBetsButton.onclick = () => { showSettings = false; showBetsPanel = false; showEditBetsPanel = true; refreshAll(); }; settingsScrollContainer.appendChild(editBetsButton); const chargedshootsToggleBtn = document.createElement('button'); const updateChargedshootsButtonText = () => { chargedshootsToggleBtn.textContent = `⚡ Charging Shoots: ${isChargedshootsEnabled ? 'On' : 'Off'}`; }; updateChargedshootsButtonText(); chargedshootsToggleBtn.title = "Toggle the 'Charging Shoots' helper panel. When ON, you can pre-load shoots to be fired automatically in a match."; Object.assign(chargedshootsToggleBtn.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); chargedshootsToggleBtn.onmouseenter = () => chargedshootsToggleBtn.style.background = 'rgba(255,255,255,0.2)'; chargedshootsToggleBtn.onmouseleave = () => chargedshootsToggleBtn.style.background = 'rgba(255,255,255,0.1)'; chargedshootsToggleBtn.onclick = () => { isChargedshootsEnabled = !isChargedshootsEnabled; localStorage.setItem(CHARGED_SHOOTS_ENABLED_KEY, JSON.stringify(isChargedshootsEnabled)); updateChargedshootsButtonText(); updateChargedshootsPanelVisibility(); }; settingsScrollContainer.appendChild(chargedshootsToggleBtn); const resetBtn = document.createElement('button'); resetBtn.textContent = '🔄 Reset Data'; resetBtn.title = "WARNING: This will clear all tracked stats, profit, and reset all bet configurations to default."; Object.assign(resetBtn.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); resetBtn.onmouseenter = () => resetBtn.style.background = 'rgba(255,255,255,0.2)'; resetBtn.onmouseleave = () => resetBtn.style.background = 'rgba(255,255,255,0.1)'; resetBtn.onclick = () => { if (confirm('Clear all results and reset profit? This will also reset all bet configurations and your API key.')) { localStorage.removeItem(STORAGE); localStorage.removeItem(PROFIT_STORAGE); localStorage.removeItem(ALERT_SHOWN_PROFIT_KEY); localStorage.removeItem(ALERT_SHOWN_LOSS_KEY); localStorage.removeItem(CHARGED_SHOOTS_COUNT_KEY); localStorage.removeItem(MANUAL_BETS_STORAGE_KEY); localStorage.removeItem(DYNAMIC_BETS_ENABLED_KEY); localStorage.removeItem(DYNAMIC_BETS_SETTINGS_KEY); localStorage.removeItem(UI_BUTTON_COUNT_KEY); localStorage.removeItem(PANEL_SCALE_KEY); localStorage.removeItem(API_KEY_STORAGE); localStorage.removeItem(LAST_SYNC_KEY); initializeState(); apiKeyInput.value = ''; refreshAll(); } }; settingsScrollContainer.appendChild(resetBtn); const autoHideBtn = document.createElement('button'); autoHideBtn.title = "When ON, the tracker will automatically hide when you are not on a Russian Roulette page."; const updateAutoHideBtnText = () => { autoHideBtn.textContent = autoHide ? 'Auto-Hide: On' : 'Auto-Hide: Off'; } updateAutoHideBtnText(); Object.assign(autoHideBtn.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); autoHideBtn.onmouseenter = () => autoHideBtn.style.background = 'rgba(255,255,255,0.2)'; autoHideBtn.onmouseleave = () => autoHideBtn.style.background = 'rgba(255,255,255,0.1)'; autoHideBtn.onclick = () => { autoHide = !autoHide; localStorage.setItem(AUTOHIDE_KEY, JSON.stringify(autoHide)); updateAutoHideBtnText(); refreshAll(); }; settingsScrollContainer.appendChild(autoHideBtn); const buttonContainer = document.createElement('div'); Object.assign(buttonContainer.style, { display: 'flex', gap: '4px', marginTop: '4px' }); panel.appendChild(buttonContainer); const settingsButton = document.createElement('button'); settingsButton.textContent = '⚙️ Settings'; settingsButton.title = "Open the settings panel."; Object.assign(settingsButton.style, { flex: '1', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); settingsButton.onmouseenter = () => settingsButton.style.background = 'rgba(255,255,255,0.2)'; settingsButton.onmouseleave = () => settingsButton.style.background = 'rgba(255,255,255,0.1)'; settingsButton.onclick = () => { showSettings = !showSettings; showBetsPanel = false; showEditBetsPanel = false; showStatsPanel = false; refreshAll(); }; buttonContainer.appendChild(settingsButton); const betsPanel = document.createElement('div'); Object.assign(betsPanel.style, { display: 'none', flexDirection: 'column', gap: '8px', padding: '12px 0' }); panel.appendChild(betsPanel); // ... (betsPanel UI is unchanged, assume it's here) ... const betsPanelLastMatchesTitle = document.createElement('div'); betsPanelLastMatchesTitle.textContent = `Last ${miniBarCount} Matches`; Object.assign(betsPanelLastMatchesTitle.style, { fontSize: '13px', color: 'rgba(255,255,255,0.8)', textAlign: 'center', marginBottom: '-4px' }); const betsPanelCirclesContainer = document.createElement('div'); Object.assign(betsPanelCirclesContainer.style, { display: 'flex', flexWrap: 'wrap', gap: '3px', justifyContent: 'center', marginBottom: '6px' }); betsPanel.appendChild(betsPanelLastMatchesTitle); betsPanel.appendChild(betsPanelCirclesContainer); const betsTitle = document.createElement('div'); betsTitle.textContent = 'Smart Bets'; Object.assign(betsTitle.style, { fontSize: '16px', fontWeight: 'bold', marginBottom: '4px', textAlign: 'center' }); betsPanel.appendChild(betsTitle); const backButtonBets = document.createElement('button'); backButtonBets.textContent = '← Back'; Object.assign(backButtonBets.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer', marginBottom: '8px' }); backButtonBets.onmouseenter = () => backButtonBets.style.background = 'rgba(255,255,255,0.2)'; backButtonBets.onmouseleave = () => backButtonBets.style.background = 'rgba(255,255,255,0.1)'; backButtonBets.onclick = () => { showBetsPanel = false; showSettings = false; showEditBetsPanel = false; refreshAll(); }; betsPanel.appendChild(backButtonBets); const betButtonsContainer = document.createElement('div'); Object.assign(betButtonsContainer.style, { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '6px' }); betsPanel.appendChild(betButtonsContainer); const editBetsPanel = document.createElement('div'); Object.assign(editBetsPanel.style, { display: 'none', flexDirection: 'column', gap: '8px', padding: '12px 0' }); panel.appendChild(editBetsPanel); // ... (editBetsPanel UI is unchanged, assume it's here) ... const editBetsTitle = document.createElement('div'); editBetsTitle.textContent = 'Edit Bets'; Object.assign(editBetsTitle.style, { fontSize: '16px', fontWeight: 'bold', marginBottom: '4px' }); editBetsPanel.appendChild(editBetsTitle); const backButtonEditBets = document.createElement('button'); backButtonEditBets.textContent = '← Back to Settings'; Object.assign(backButtonEditBets.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer', marginBottom: '8px' }); backButtonEditBets.onmouseenter = () => backButtonEditBets.style.background = 'rgba(255,255,255,0.2)'; backButtonEditBets.onmouseleave = () => backButtonEditBets.style.background = 'rgba(255,255,255,0.1)'; backButtonEditBets.onclick = () => { showEditBetsPanel = false; showSettings = true; refreshAll(); }; editBetsPanel.appendChild(backButtonEditBets); const dynamicBetsToggleContainer = document.createElement('div'); Object.assign(dynamicBetsToggleContainer.style, { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px', background: 'rgba(255,255,255,0.05)', borderRadius: '6px' }); const dynamicBetsToggle = document.createElement('input'); dynamicBetsToggle.type = 'checkbox'; dynamicBetsToggle.id = 'dynamic-bets-toggle'; dynamicBetsToggle.checked = dynamicBetsEnabled; const dynamicBetsLabel = document.createElement('label'); dynamicBetsLabel.textContent = 'Enable Dynamic Bets'; dynamicBetsLabel.htmlFor = 'dynamic-bets-toggle'; dynamicBetsLabel.style.cursor = 'pointer'; dynamicBetsToggleContainer.title = "Toggle between setting manual bet amounts and using the dynamic bet calculator."; dynamicBetsToggleContainer.append(dynamicBetsToggle, dynamicBetsLabel); editBetsPanel.appendChild(dynamicBetsToggleContainer); const dynamicBetsContainer = document.createElement('div'); Object.assign(dynamicBetsContainer.style, { display: dynamicBetsEnabled ? 'grid' : 'none', gridTemplateColumns: 'auto 1fr auto', gap: '8px 10px', marginTop: '10px' }); editBetsPanel.appendChild(dynamicBetsContainer); const manualBetsContainer = document.createElement('div'); Object.assign(manualBetsContainer.style, { display: dynamicBetsEnabled ? 'none' : 'block', marginTop: '10px' }); editBetsPanel.appendChild(manualBetsContainer); const manualBetsTitle = document.createElement('div'); manualBetsTitle.textContent = 'Manual Bet Configuration'; Object.assign(manualBetsTitle.style, { fontSize: '13px', color: 'rgba(255,255,255,0.7)', marginBottom: '8px' }); manualBetsContainer.appendChild(manualBetsTitle); const manualEditGrid = document.createElement('div'); Object.assign(manualEditGrid.style, { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '6px 10px', overflowY: 'auto', maxHeight: '200px' }); manualBetsContainer.appendChild(manualEditGrid); const createDynamicInput = (label, id, value, title, type = 'text') => { const lbl = document.createElement('label'); lbl.textContent = label; lbl.htmlFor = id; const input = document.createElement('input'); input.type = type; input.id = id; input.value = value; input.title = title; Object.assign(input.style, { width: '100%', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); const lock = document.createElement('button'); lock.style.cursor = 'pointer'; lock.style.background = 'none'; lock.style.border = 'none'; lock.style.color = '#fff'; lock.style.fontSize = '16px'; lock.style.padding = '0 5px'; return [lbl, input, lock]; }; const [testBetLabel, localTestBetInput] = createDynamicInput('Test Bet:', 'dyn-test-bet', dynamicBetsSettings.testBet, "A small, fixed bet for the first UI button. Accepts k/m/b suffixes.", 'text'); testBetInput = localTestBetInput; testBetInput.onchange = () => { const parsed = parseBetInput(testBetInput.value); if (parsed !== null) { dynamicBetsSettings.testBet = Math.max(0, parsed); saveDynamicBetsSettings(); calculateBetsAndRefresh(); } else { testBetInput.value = formatNumberToKMB(dynamicBetsSettings.testBet); } }; dynamicBetsContainer.append(testBetLabel, testBetInput, document.createElement('div')); const [capitalLabel, localCapitalInput, localCapitalLock] = createDynamicInput('Gambling Capital:', 'dyn-capital', dynamicBetsSettings.capital, "Define your total risk. Lock this (🔒) to have the script calculate the 'Starting Bet' for you. Accepts k/m/b suffixes.", 'text'); capitalInput = localCapitalInput; capitalLock = localCapitalLock; capitalInput.onchange = () => { if (dynamicBetsSettings.lockedField === 'capital') { const parsed = parseBetInput(capitalInput.value); if (parsed !== null) { dynamicBetsSettings.capital = Math.max(0, parsed); saveDynamicBetsSettings(); calculateBetsAndRefresh(); } else { capitalInput.value = formatNumberToKMB(dynamicBetsSettings.capital); } } }; capitalLock.onclick = () => { dynamicBetsSettings.lockedField = 'capital'; saveDynamicBetsSettings(); calculateBetsAndRefresh(); }; dynamicBetsContainer.append(capitalLabel, capitalInput, capitalLock); const [startingBetLabel, localStartingBetInput, localStartingBetLock] = createDynamicInput('Starting Bet (x):', 'dyn-starting-bet', dynamicBetsSettings.startingBet, "Define your ideal first bet. Lock this (🔒) to have the script calculate the total 'Gambling Capital' required. Accepts k/m/b suffixes.", 'text'); startingBetInput = localStartingBetInput; startingBetLock = localStartingBetLock; startingBetInput.onchange = () => { if (dynamicBetsSettings.lockedField === 'startingBet') { const parsed = parseBetInput(startingBetInput.value); if (parsed !== null) { dynamicBetsSettings.startingBet = Math.max(0, parsed); saveDynamicBetsSettings(); calculateBetsAndRefresh(); } else { startingBetInput.value = formatNumberToKMB(dynamicBetsSettings.startingBet); } } }; startingBetLock.onclick = () => { dynamicBetsSettings.lockedField = 'startingBet'; saveDynamicBetsSettings(); calculateBetsAndRefresh(); }; dynamicBetsContainer.append(startingBetLabel, startingBetInput, startingBetLock); const [maxLLabel, maxLInput] = createDynamicInput('Max L Streak:', 'dyn-max-l', dynamicBetsSettings.maxLStreak, `The maximum number of consecutive losses your strategy should be able to withstand. Max: ${MAX_L_STREAK_CAP}.`, 'number'); maxLInput.max = MAX_L_STREAK_CAP; maxLInput.onchange = () => { let val = Math.max(1, parseInt(maxLInput.value, 10) || 1); val = Math.min(val, MAX_L_STREAK_CAP); dynamicBetsSettings.maxLStreak = val; maxLInput.value = val; saveDynamicBetsSettings(); calculateBetsAndRefresh(); }; dynamicBetsContainer.append(maxLLabel, maxLInput, document.createElement('div')); const [martingaleLabel, martingaleInput] = createDynamicInput('Martingale Value:', 'dyn-martingale', dynamicBetsSettings.martingaleValue, "The multiplier for your bets (e.g., 2 for doubling, 3 for tripling).", 'number'); martingaleInput.step = '0.1'; martingaleInput.onchange = () => { let val = Math.max(1.1, parseFloat(martingaleInput.value) || 1.1); dynamicBetsSettings.martingaleValue = val; martingaleInput.value = val; saveDynamicBetsSettings(); calculateBetsAndRefresh(); }; dynamicBetsContainer.append(martingaleLabel, martingaleInput, document.createElement('div')); const reinforceContainer = document.createElement('div'); Object.assign(reinforceContainer.style, { gridColumn: '1 / -1', display: 'flex', alignItems: 'center', gap: '8px' }); const reinforceCheck = document.createElement('input'); reinforceCheck.type = 'checkbox'; reinforceCheck.id = 'dyn-reinforce'; reinforceCheck.checked = dynamicBetsSettings.reinforce; const reinforceLabel = document.createElement('label'); reinforceLabel.textContent = 'Reinforce Bet'; reinforceLabel.htmlFor = 'dyn-reinforce'; reinforceLabel.style.cursor = 'pointer'; reinforceContainer.title = "If checked, adds the Starting Bet back into the calculation on odd-numbered bets (3rd, 5th, etc.) for a more aggressive progression."; reinforceCheck.onchange = () => { dynamicBetsSettings.reinforce = reinforceCheck.checked; saveDynamicBetsSettings(); calculateBetsAndRefresh(); }; reinforceContainer.append(reinforceCheck, reinforceLabel); dynamicBetsContainer.appendChild(reinforceContainer); dynamicBetsToggle.onchange = () => { dynamicBetsEnabled = dynamicBetsToggle.checked; localStorage.setItem(DYNAMIC_BETS_ENABLED_KEY, JSON.stringify(dynamicBetsEnabled)); dynamicBetsContainer.style.display = dynamicBetsEnabled ? 'grid' : 'none'; manualBetsContainer.style.display = dynamicBetsEnabled ? 'none' : 'block'; calculateBetsAndRefresh(); }; const betsButton = document.createElement('button'); betsButton.textContent = '💰 Bets'; betsButton.title = "Open the Smart Bets panel to start a match."; Object.assign(betsButton.style, { flex: '1', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer'}); betsButton.onmouseenter = () => betsButton.style.background = 'rgba(255,255,255,0.2)'; betsButton.onmouseleave = () => betsButton.style.background = 'rgba(255,255,255,0.1)'; betsButton.onclick = () => { showBetsPanel = !showBetsPanel; showSettings = false; showEditBetsPanel = false; showStatsPanel = false; refreshAll(); }; buttonContainer.appendChild(betsButton); // --- NEW: Stats Panel UI --- const statsButton = document.createElement('button'); statsButton.textContent = '📊 Stats'; statsButton.title = "Open the Statistics and Graph panel."; Object.assign(statsButton.style, { flex: '1', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer' }); statsButton.onmouseenter = () => statsButton.style.background = 'rgba(255,255,255,0.2)'; statsButton.onmouseleave = () => statsButton.style.background = 'rgba(255,255,255,0.1)'; statsButton.onclick = () => { showStatsPanel = !showStatsPanel; showSettings = false; showBetsPanel = false; showEditBetsPanel = false; refreshAll(); if (showStatsPanel) { updateGraph(); // Draw graph when panel is opened } }; buttonContainer.appendChild(statsButton); const statsDisplayPanel = document.createElement('div'); Object.assign(statsDisplayPanel.style, { display: 'none', flexDirection: 'column', gap: '8px', padding: '12px 0', width: '360px' }); panel.appendChild(statsDisplayPanel); const statsDisplayTitle = document.createElement('div'); statsDisplayTitle.textContent = 'Statistics & Graph'; Object.assign(statsDisplayTitle.style, { fontSize: '16px', fontWeight: 'bold' }); statsDisplayPanel.appendChild(statsDisplayTitle); const backButtonStats = document.createElement('button'); backButtonStats.textContent = '← Back'; Object.assign(backButtonStats.style, { alignSelf: 'flex-start', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 8px', cursor: 'pointer', marginBottom: '8px' }); backButtonStats.onmouseenter = () => backButtonStats.style.background = 'rgba(255,255,255,0.2)'; backButtonStats.onmouseleave = () => backButtonStats.style.background = 'rgba(255,255,255,0.1)'; backButtonStats.onclick = () => { showStatsPanel = false; refreshAll(); }; statsDisplayPanel.appendChild(backButtonStats); const statsSummaryDiv = document.createElement('div'); Object.assign(statsSummaryDiv.style, { fontSize: '12px', display: 'flex', justifyContent: 'space-around', padding: '4px', background: 'rgba(255,255,255,0.05)', borderRadius: '4px' }); statsDisplayPanel.appendChild(statsSummaryDiv); const graphCanvas = document.createElement('canvas'); graphCanvas.id = 'rr-graph-canvas'; graphCanvas.width = 350; graphCanvas.height = 200; Object.assign(graphCanvas.style, { background: 'rgba(0,0,0,0.2)', borderRadius: '6px', marginTop: '8px' }); statsDisplayPanel.appendChild(graphCanvas); const timeframeContainer = document.createElement('div'); Object.assign(timeframeContainer.style, { display: 'flex', justifyContent: 'center', gap: '5px', marginTop: '8px' }); statsDisplayPanel.appendChild(timeframeContainer); const timeframes = [ { label: '7D', days: 7 }, { label: '1M', days: 30 }, { label: '3M', days: 90 }, { label: '6M', days: 182 }, { label: '1Y', days: 365 } ]; timeframes.forEach(tf => { const btn = document.createElement('button'); btn.textContent = tf.label; btn.dataset.days = tf.days; btn.title = `Show stats for the last ${tf.label}`; Object.assign(btn.style, { background: 'rgba(255,255,255,0.1)', color: '#fff', border: '1px solid #555', borderRadius: '6px', padding: '3px 8px', cursor: 'pointer', fontSize: '11px' }); btn.onmouseenter = () => btn.style.background = 'rgba(255,255,255,0.2)'; btn.onmouseleave = () => { if(currentStatsTimeframe != tf.days) btn.style.background = 'rgba(255,255,255,0.1)'; }; btn.onclick = () => { currentStatsTimeframe = tf.days; updateGraph(); }; timeframeContainer.appendChild(btn); }); // --- Core Functions --- const saveResults = () => localStorage.setItem(STORAGE, JSON.stringify(results)); const saveTotalProfit = () => localStorage.setItem(PROFIT_STORAGE, totalProfit.toString()); const saveCollapsed = () => localStorage.setItem(COLLAPSE_KEY, JSON.stringify(collapsed)); const saveChargedshoots = () => localStorage.setItem(CHARGED_SHOOTS_COUNT_KEY, chargedshoots.toString()); const saveManualBets = () => localStorage.setItem(MANUAL_BETS_STORAGE_KEY, JSON.stringify(manualBets)); const saveDynamicBetsSettings = () => localStorage.setItem(DYNAMIC_BETS_SETTINGS_KEY, JSON.stringify(dynamicBetsSettings)); // --- NEW/MODIFIED: Graphing and Stats Functions --- /** * Updates the summary text and redraws the graph for the current timeframe. */ function updateGraph() { const { dataPoints, summary } = processDataForGraph(currentStatsTimeframe); // Update summary text const profitSign = summary.totalProfit >= 0 ? '+' : '-'; const profitColor = summary.totalProfit >= 0 ? '#4CAF50' : '#E53935'; statsSummaryDiv.innerHTML = ` <span title="Total profit/loss for this period" style="color:${profitColor};">${profitSign}$${formatNumberToKMB(Math.abs(summary.totalProfit))}</span> <span title="Wins / Losses">W/L: ${summary.wins} / ${summary.losses}</span> <span title="Total games played">Games: ${summary.totalGames}</span> `; drawGraph(graphCanvas, dataPoints, summary); // Highlight active timeframe button timeframeContainer.querySelectorAll('button').forEach(btn => { if (parseInt(btn.dataset.days, 10) === currentStatsTimeframe) { btn.style.background = 'rgba(100, 180, 255, 0.3)'; btn.style.borderColor = '#64B4FF'; } else { btn.style.background = 'rgba(255,255,255,0.1)'; btn.style.borderColor = '#555'; } }); } /** * Processes raw results data into a format suitable for graphing. * @param {number} timeframeInDays - The number of days of history to process. * @returns {{dataPoints: Array<{timestamp: number, cumulativeProfit: number}>, summary: object}} */ /** * Processes raw results data into a format suitable for graphing. * @param {number} timeframeInDays - The number of days of history to process. * @returns {{dataPoints: Array<{timestamp: number, cumulativeProfit: number}>, summary: object}} */ function processDataForGraph(timeframeInDays) { const now = Math.floor(Date.now() / 1000); const cutoffTimestamp = now - (timeframeInDays * 24 * 60 * 60); const filteredResults = results.filter(r => r.timestamp >= cutoffTimestamp); if (filteredResults.length === 0) { return { dataPoints: [], summary: { totalProfit: 0, wins: 0, losses: 0, totalGames: 0 } }; } // Sort oldest to newest for cumulative calculation filteredResults.sort((a, b) => a.timestamp - b.timestamp); let cumulativeProfit = 0; let wins = 0; let losses = 0; const dataPoints = filteredResults.map(game => { const profit = game.result === 'win' ? game.bet : -game.bet; cumulativeProfit += profit; if (profit > 0) wins++; else losses++; return { timestamp: game.timestamp, cumulativeProfit: cumulativeProfit }; }); // Add a starting point at the beginning of the period if (dataPoints.length > 0) { const firstGame = filteredResults[0]; // FIX: Use the original game data const profitOfFirstGame = firstGame.result === 'win' ? firstGame.bet : -firstGame.bet; const startingProfit = dataPoints[0].cumulativeProfit - profitOfFirstGame; dataPoints.unshift({ timestamp: firstGame.timestamp - 1, cumulativeProfit: startingProfit }); } const summary = { totalProfit: cumulativeProfit, wins: wins, losses: losses, totalGames: wins + losses, }; return { dataPoints, summary }; } /** * Draws the profit graph on the provided canvas element. * @param {HTMLCanvasElement} canvas * @param {Array<{timestamp: number, cumulativeProfit: number}>} data */ /** * Draws the profit graph on the provided canvas element. * @param {HTMLCanvasElement} canvas * @param {Array<{timestamp: number, cumulativeProfit: number}>} data */ function drawGraph(canvas, data) { const ctx = canvas.getContext('2d'); const { width, height } = canvas; const padding = { top: 20, right: 10, bottom: 20, left: 50 }; ctx.clearRect(0, 0, width, height); if (data.length < 2) { ctx.fillStyle = '#888'; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Not enough data for this period.', width / 2, height / 2); return; } const profits = data.map(p => p.cumulativeProfit); const timestamps = data.map(p => p.timestamp); let minProfit = Math.min(...profits); let maxProfit = Math.max(...profits); const minTimestamp = Math.min(...timestamps); const maxTimestamp = Math.max(...timestamps); // Add buffer to min/max profit for nice spacing, ensuring 0 is visible const range = maxProfit - minProfit; if (range === 0) { // Handle case where profit is flat maxProfit += 1000; minProfit -= 1000; } maxProfit = Math.max(maxProfit + range * 0.1, 0); minProfit = Math.min(minProfit - range * 0.1, 0); const mapX = (ts) => padding.left + (ts - minTimestamp) / (maxTimestamp - minTimestamp) * (width - padding.left - padding.right); const mapY = (profit) => height - padding.bottom - (profit - minProfit) / (maxProfit - minProfit) * (height - padding.top - padding.bottom); // Draw Axes ctx.strokeStyle = '#555'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(padding.left, padding.top); ctx.lineTo(padding.left, height - padding.bottom); ctx.lineTo(width - padding.right, height - padding.bottom); ctx.stroke(); // Draw Y-axis labels (Profit) ctx.fillStyle = '#aaa'; ctx.font = '10px monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; const yLabelCount = 5; for (let i = 0; i <= yLabelCount; i++) { const profitValue = minProfit + (i / yLabelCount) * (maxProfit - minProfit); const y = mapY(profitValue); ctx.fillText(formatNumberToKMB(profitValue), padding.left - 5, y); } // Draw zero line const zeroY = mapY(0); if (zeroY > padding.top && zeroY < height - padding.bottom) { ctx.setLineDash([2, 3]); ctx.strokeStyle = '#777'; ctx.beginPath(); ctx.moveTo(padding.left, zeroY); ctx.lineTo(width - padding.right, zeroY); ctx.stroke(); ctx.setLineDash([]); } // --- START OF REPLACEMENT DRAWING LOGIC --- ctx.lineWidth = 2; for (let i = 1; i < data.length; i++) { const p1 = data[i - 1]; const p2 = data[i]; const p1_is_positive = p1.cumulativeProfit >= 0; const p2_is_positive = p2.cumulativeProfit >= 0; // Start a new path for each segment ctx.beginPath(); ctx.moveTo(mapX(p1.timestamp), mapY(p1.cumulativeProfit)); if (p1_is_positive !== p2_is_positive) { // Line crosses the zero axis, split it into two colored parts const intersect_ts = p1.timestamp + (p2.timestamp - p1.timestamp) * (0 - p1.cumulativeProfit) / (p2.cumulativeProfit - p1.cumulativeProfit); const x_intersect = mapX(intersect_ts); const y_intersect = mapY(0); // Draw first part ctx.lineTo(x_intersect, y_intersect); ctx.strokeStyle = p1_is_positive ? '#4CAF50' : '#E53935'; ctx.stroke(); // Draw second part ctx.beginPath(); ctx.moveTo(x_intersect, y_intersect); ctx.lineTo(mapX(p2.timestamp), mapY(p2.cumulativeProfit)); ctx.strokeStyle = p2_is_positive ? '#4CAF50' : '#E53935'; ctx.stroke(); } else { // Line does not cross zero, draw as a single colored segment ctx.lineTo(mapX(p2.timestamp), mapY(p2.cumulativeProfit)); ctx.strokeStyle = p1_is_positive ? '#4CAF50' : '#E53935'; ctx.stroke(); } } // --- END OF REPLACEMENT DRAWING LOGIC --- } // --- (Rest of the original functions: API handling, bet calculations, UI building, etc.) --- async function fetchAllLogsPaginated(logType) { const allLogs = []; let currentToTimestamp = null; let keepFetching = true; const stopTimestamp = Math.floor(Date.now() / 1000) - (30000 * 24 * 3600); // 30k days ago console.log(`RR Tracker: Paginating log type ${logType}, fetching history up to 30k days ago.`); while (keepFetching) { try { const batch = await fetchLogs(logType, currentToTimestamp); const logsInBatch = Object.values(batch.log || {}); if (logsInBatch.length === 0) { keepFetching = false; break; } let oldestTimestampInBatch = Infinity; for (const log of logsInBatch) { if (log.timestamp < oldestTimestampInBatch) { oldestTimestampInBatch = log.timestamp; } if (log.timestamp < stopTimestamp) { keepFetching = false; } allLogs.push(log); } if (logsInBatch.length < 100) { keepFetching = false; } if (!keepFetching) break; currentToTimestamp = oldestTimestampInBatch - 1; await new Promise(resolve => setTimeout(resolve, 400)); } catch (error) { console.error(`RR Tracker: Error during log pagination for type ${logType}:`, error); keepFetching = false; throw error; } } return allLogs.filter(log => log.timestamp >= stopTimestamp); } async function importApiData(isSilent = false) { if (!apiKey || apiKey.length < 16) { if (!isSilent) alert('Please enter a valid Torn API key in the settings panel to sync data.'); return; } console.log("RR Tracker: Starting comprehensive API sync..."); try { const existingTimestamps = new Set(results.map(r => r.timestamp)); const [winLogsRaw, loseLogsRaw] = await Promise.all([ fetchAllLogsPaginated(8395), fetchAllLogsPaginated(8396) ]); const processedWins = winLogsRaw.map(log => ({ result: 'win', bet: log.data.pot / 2, timestamp: log.timestamp })); const processedLosses = loseLogsRaw.map(log => ({ result: 'lose', bet: log.data.pot / 2, timestamp: log.timestamp })); const allFetchedLogs = [...processedWins, ...processedLosses]; const newUniqueLogs = allFetchedLogs.filter(log => !existingTimestamps.has(log.timestamp)); if (newUniqueLogs.length === 0) { console.log("RR Tracker: No new match data found in logs."); if (!isSilent) alert("Sync complete: No new Russian Roulette games found."); lastSyncTime = Date.now(); localStorage.setItem(LAST_SYNC_KEY, lastSyncTime.toString()); return; } let combinedResults = [...results, ...newUniqueLogs]; combinedResults.sort((a, b) => b.timestamp - a.timestamp); if (combinedResults.length > maxDisplayMatches) { results = combinedResults.slice(0, maxDisplayMatches); } else { results = combinedResults; } totalProfit = results.reduce((acc, game) => acc + (game.result === 'win' ? game.bet : -game.bet), 0); saveResults(); saveTotalProfit(); console.log(`RR Tracker: Sync complete! Added ${newUniqueLogs.length} new games. Total tracked: ${results.length}.`); if (!isSilent) { alert(`Sync complete! ${newUniqueLogs.length} new games were added.\nTotal games tracked is now ${results.length}.`); } refreshAll(); } catch (error) { console.error("RR Tracker API Sync Error:", error); if (!isSilent) alert(`API Sync Error: ${error.message}. Please check your API key and console (F12) for details.`); } finally { lastSyncTime = Date.now(); localStorage.setItem(LAST_SYNC_KEY, lastSyncTime.toString()); } } function fetchLogs(logType, toTimestamp = null) { let url = `https://api.torn.com/user/?selections=log&log=${logType}&key=${apiKey}`; if (toTimestamp) { url += `&to=${Math.floor(toTimestamp)}`; } return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "GET", url: url, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.error) { reject(new Error(data.error.error)); } else { resolve(data); } } catch (e) { reject(new Error("Failed to parse API response.")); } }, onerror: (err) => reject(new Error("Network request failed.")), ontimeout: () => reject(new Error("Request timed out.")), }); }); } let syncInterval = null; function initiateSyncing() { if (syncInterval) clearInterval(syncInterval); if (!apiKey || apiKey.length < 16) { console.log("RR Tracker: No API key set, auto-sync disabled."); return; } const oneMinute = 60 * 1000; if (Date.now() - lastSyncTime > oneMinute) { console.log("RR Tracker: Stale data detected, performing initial sync."); importApiData(true); } else { console.log("RR Tracker: Last sync was recent, skipping initial sync."); } syncInterval = setInterval(() => importApiData(true), oneMinute); console.log("RR Tracker: Auto-sync initiated for every 60 seconds."); } function calculateBetsAndRefresh() { calculateBets(); refreshAll(); } function updateChargedshootsDisplay() { if (chargedshootsDisplay) chargedshootsDisplay.textContent = `Charges: ${chargedshoots} / 3`; } function updateChargedshootsPanelVisibility() { const inMatch = document.body.innerText.includes('POT MONEY') && !document.body.innerText.includes('Create Game'); if (isChargedshootsEnabled && inMatch) { chargedshootsPanel.style.display = 'flex'; chargedshootsPanel.style.background = `rgba(0,0,0,${currentOpacity})`; } else { chargedshootsPanel.style.display = 'none'; } } function performAutoShoot() { if (!isChargedshootsEnabled || chargedshoots <= 0 || isShootDelayed) return; const shootButton = [...document.querySelectorAll('button.torn-btn, button.btn')].find(b => b.textContent.trim().toLowerCase() === 'shoot'); if (shootButton && !shootButton.disabled && shootButton.offsetParent !== null) { isShootDelayed = true; setTimeout(() => { const currentShootButton = [...document.querySelectorAll('button.torn-btn, button.btn')].find(b => b.textContent.trim().toLowerCase() === 'shoot'); if (currentShootButton && !currentShootButton.disabled && currentShootButton.offsetParent !== null) { currentShootButton.click(); chargedshoots--; saveChargedshoots(); updateChargedshootsDisplay(); } isShootDelayed = false; }, 500); } } function parseBetInput(str) { if (typeof str !== 'string') str = String(str); str = str.toLowerCase().replace(/,/g, '').trim(); let m = 1; if (str.endsWith('k')) { m = 1e3; str = str.slice(0, -1); } else if (str.endsWith('m')) { m = 1e6; str = str.slice(0, -1); } else if (str.endsWith('b')) { m = 1e9; str = str.slice(0, -1); } let v = parseFloat(str); return (isNaN(v) || v < 0) ? null : Math.floor(v * m); } function formatNumberToKMB(num) { if (num === null || isNaN(num)) return ''; if (num >= 1e9) return (num / 1e9).toFixed(2).replace(/\.00$/, '').replace(/\.([1-9])0$/, '.$1') + 'b'; if (num >= 1e6) return (num / 1e6).toFixed(2).replace(/\.00$/, '').replace(/\.([1-9])0$/, '.$1') + 'm'; if (num >= 1e3) return (num / 1e3).toFixed(2).replace(/\.00$/, '').replace(/\.([1-9])0$/, '.$1') + 'k'; return num.toLocaleString(); } function loadManualBets() { try { const storedJson = localStorage.getItem(MANUAL_BETS_STORAGE_KEY); if (storedJson) { const parsed = JSON.parse(storedJson); if (Array.isArray(parsed)) return parsed; } } catch (e) { console.error("Error parsing stored manual bets:", e); } return [...DEFAULT_MANUAL_BETS]; } function adjustManualBetsArray() { const currentLength = manualBets.length; if (currentLength === uiButtonCount) return; if (currentLength < uiButtonCount) { const lastValue = currentLength > 0 ? manualBets[currentLength - 1] : 10000; for (let i = currentLength; i < uiButtonCount; i++) { manualBets.push(lastValue * Math.pow(2, i - currentLength + 1)); } } else { manualBets = manualBets.slice(0, uiButtonCount); } saveManualBets(); } function buildManualBetInputs() { manualEditGrid.innerHTML = ''; manualEditInputs = []; for (let i = 0; i < uiButtonCount; i++) { const label = document.createElement('label'); label.textContent = `Bet ${i + 1}:`; Object.assign(label.style, { fontSize: '13px', paddingTop: '4px' }); manualEditGrid.appendChild(label); const input = document.createElement('input'); input.type = 'text'; input.placeholder = 'e.g., 10k'; input.title = `Set the manual bet amount for UI button ${i + 1}.`; Object.assign(input.style, { width: '80px', padding: '4px', border: '1px solid #555', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: '#fff' }); input.dataset.index = i; input.onchange = (e) => { const index = parseInt(e.target.dataset.index, 10); const parsed = parseBetInput(e.target.value); if (parsed !== null) { manualBets[index] = parsed; saveManualBets(); e.target.value = formatNumberToKMB(parsed); calculateBetsAndRefresh(); } else if (e.target.value !== '') { alert('Invalid input. Please use numbers or suffixes like K/M/B (e.g., 25k, 2.5m).'); e.target.value = formatNumberToKMB(manualBets[index]); } }; manualEditGrid.appendChild(input); manualEditInputs.push(input); } } buildManualBetInputs(); function startMatch(originalButtonBetValue) { const inputBox = document.querySelector('input[aria-label="Money value"]'); if (inputBox) { nativeSet.call(inputBox, originalButtonBetValue.toLocaleString('en-US')); inputBox.dispatchEvent(new Event('input', { bubbles: true })); inputBox.dispatchEvent(new Event('change', { bubbles: true })); const betAfterInputSet = parseBetInput(inputBox.value); if (betAfterInputSet === null || betAfterInputSet <= 0) { alert("Invalid bet amount currently in the input box. Please ensure it's a valid number."); return; } if (betAfterInputSet < originalButtonBetValue) { alert(`The bet amount in the box ($${betAfterInputSet.toLocaleString()}) is less than the button's intended value ($${originalButtonBetValue.toLocaleString()}).\nMatch not started.`); return; } let startButton = [...document.querySelectorAll('button')].find(b => b.textContent.trim().toLowerCase() === 'start'); if (startButton) startButton.click(); } else { setTimeout(() => { let yesButton = [...document.querySelectorAll('button')].find(b => b.textContent.trim().toLowerCase() === 'yes'); if (yesButton) yesButton.click(); }, 400); } } function getSumOfRatios(L, M, reinforce, numCoeffs) { if (numCoeffs <= 0 || M <= 1) return { sum: 0, coeffs: [] }; const maxCoeffs = Math.max(L, numCoeffs); const allCoeffs = []; if (!reinforce) { for (let i = 0; i < maxCoeffs; i++) { allCoeffs.push(Math.pow(M, i)); } } else { let lastCoeff = 0; for (let i = 1; i <= maxCoeffs; i++) { let currentCoeff; if (i === 1) { currentCoeff = 1; } else if (i % 2 === 0) { currentCoeff = M * lastCoeff; } else { currentCoeff = (M * lastCoeff) + 1; } allCoeffs.push(currentCoeff); lastCoeff = currentCoeff; } } const sum = allCoeffs.slice(0, L).reduce((acc, val) => acc + val, 0); const coeffsForDisplay = allCoeffs.slice(0, numCoeffs); return { sum, coeffs: coeffsForDisplay }; } function calculateBets() { if (dynamicBetsEnabled) { const { capital, maxLStreak, martingaleValue, reinforce, lockedField } = dynamicBetsSettings; let startingBet = dynamicBetsSettings.startingBet; const numCoeffsToGenerate = uiButtonCount - 1; const { sum, coeffs } = getSumOfRatios(maxLStreak, martingaleValue, reinforce, numCoeffsToGenerate); if (lockedField === 'capital') { startingBet = (sum > 0) ? Math.floor(capital / sum) : 0; dynamicBetsSettings.startingBet = startingBet; } else { dynamicBetsSettings.capital = Math.floor(startingBet * sum); } currentBets = []; currentBets.push(dynamicBetsSettings.testBet); for (let i = 0; i < coeffs.length; i++) { const betValue = Math.floor(startingBet * coeffs[i]); currentBets.push(betValue); } } else { currentBets = [...manualBets]; } } function buildBetButtons() { betButtonsContainer.innerHTML = ''; if (!currentBets || currentBets.length !== uiButtonCount) return; const firstBetRow = document.createElement('div'); Object.assign(firstBetRow.style, { display: 'flex', justifyContent: 'center', width: '100%' }); let b0 = document.createElement('button'); b0.textContent = formatNumberToKMB(currentBets[0]); b0.title = `$${currentBets[0].toLocaleString()}`; Object.assign(b0.style, { background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 10px', cursor: 'pointer', fontSize: '11px', minWidth: '80px' }); b0.onmouseenter = () => b0.style.background = 'rgba(255,255,255,0.2)'; b0.onmouseleave = () => b0.style.background = 'rgba(255,255,255,0.1)'; b0.onclick = () => startMatch(currentBets[0]); firstBetRow.appendChild(b0); betButtonsContainer.appendChild(firstBetRow); for (let i = 1; i < uiButtonCount; i += 2) { const row = document.createElement('div'); Object.assign(row.style, { display: 'flex', justifyContent: 'space-around', gap: '6px', width: '100%' }); let b1 = document.createElement('button'); b1.textContent = formatNumberToKMB(currentBets[i]); b1.title = `$${currentBets[i].toLocaleString()}`; Object.assign(b1.style, { background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 10px', cursor: 'pointer', fontSize: '11px', flex: '1', minWidth: '70px' }); b1.onmouseenter = () => b1.style.background = 'rgba(255,255,255,0.2)'; b1.onmouseleave = () => b1.style.background = 'rgba(255,255,255,0.1)'; b1.onclick = () => startMatch(currentBets[i]); row.appendChild(b1); if (i + 1 < uiButtonCount) { let b2 = document.createElement('button'); b2.textContent = formatNumberToKMB(currentBets[i + 1]); b2.title = `$${currentBets[i + 1].toLocaleString()}`; Object.assign(b2.style, { background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '4px 10px', cursor: 'pointer', fontSize: '11px', flex: '1', minWidth: '70px' }); b2.onmouseenter = () => b2.style.background = 'rgba(255,255,255,0.2)'; b2.onmouseleave = () => b2.style.background = 'rgba(255,255,255,0.1)'; b2.onclick = () => startMatch(currentBets[i + 1]); row.appendChild(b2); } betButtonsContainer.appendChild(row); } miniBar.innerHTML = ''; const miniCirclesContainer = document.createElement('div'); Object.assign(miniCirclesContainer.style, { display: 'flex', flexWrap: 'wrap', gap: '2px', justifyContent: 'center' }); results.slice(0, miniBarCount).forEach((r, idx) => miniCirclesContainer.append(makeCircle(r.result, r.bet, idx))); miniBar.appendChild(miniCirclesContainer); betsPanelCirclesContainer.innerHTML = ''; results.slice(0, miniBarCount).forEach((r, idx) => betsPanelCirclesContainer.append(makeCircle(r.result, r.bet, idx))); betsPanelLastMatchesTitle.textContent = `Last ${miniBarCount} Matches`; if (collapsed && !showBetsPanel && !showSettings && !showEditBetsPanel) { const miniBetContainer = document.createElement('div'); Object.assign(miniBetContainer.style, { display: 'flex', flexDirection: 'column', gap: '2px', alignItems: 'center' }); const buttonsPerRow = 4; let currentRow = document.createElement('div'); Object.assign(currentRow.style, { display: 'flex', flexWrap: 'nowrap', gap: '2px', justifyContent: 'center' }); miniBetContainer.appendChild(currentRow); currentBets.forEach((bet, i) => { if (i > 0 && i % buttonsPerRow === 0) { currentRow = document.createElement('div'); Object.assign(currentRow.style, { display: 'flex', flexWrap: 'nowrap', gap: '2px', justifyContent: 'center', marginTop: '2px' }); miniBetContainer.appendChild(currentRow); } let miniB = document.createElement('button'); miniB.textContent = formatNumberToKMB(bet); miniB.title = `$${bet.toLocaleString()}`; Object.assign(miniB.style, { background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', padding: '3px 6px', cursor: 'pointer', fontSize: `${miniButtonSize}px`, width: 'auto' }); miniB.onmouseenter = () => miniB.style.background = 'rgba(255,255,255,0.2)'; miniB.onmouseleave = () => miniB.style.background = 'rgba(255,255,255,0.1)'; miniB.onclick = () => startMatch(bet); currentRow.appendChild(miniB); }); miniBar.appendChild(miniBetContainer); } } const makeCircle = (result, bet, index) => { const container = document.createElement('span'); Object.assign(container.style, { display: 'inline-block', width: '14px', height: '14px', borderRadius: '50%', backgroundColor: result === 'win' ? '#4CAF50' : '#E53935', marginRight: '2px', cursor: 'pointer', position: 'relative' }); container.title = `${result.toUpperCase()}: $${bet.toLocaleString()}`; container.addEventListener('click', (e) => { e.stopPropagation(); if (isDragging || isTwoFingerDragging) return; Array.from(container.children).forEach(child => { if (child.classList.contains('rr-temp-popup')) container.removeChild(child); }); const tempPopup = document.createElement('div'); tempPopup.classList.add('rr-temp-popup'); tempPopup.textContent = `${result.toUpperCase()}: $${bet.toLocaleString()}`; Object.assign(tempPopup.style, { position: 'absolute', background: 'rgba(0,0,0,0.9)', border: '1px solid #555', color: 'white', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', whiteSpace: 'nowrap', zIndex: '10000000', top: '-28px', left: '50%', transform: 'translateX(-50%)', pointerEvents: 'none', opacity: '0', transition: 'opacity 0.2s ease-in-out' }); container.appendChild(tempPopup); setTimeout(() => tempPopup.style.opacity = '1', 10); setTimeout(() => { tempPopup.style.opacity = '0'; setTimeout(() => { if (container.contains(tempPopup)) container.removeChild(tempPopup); }, 200); }, 1500); }); return container; }; function updateStatus() { statusDiv.textContent = collapsed ? '▪' : '►'; } function updatePanelVisibility() { const onRRPage = document.body.innerText.includes('POT MONEY') || document.body.innerText.includes('Waiting:') || document.body.innerText.includes('You take your winnings') || document.body.innerText.includes('BANG! You fall down'); if (!autoHide) { panel.style.display = 'flex'; return; } panel.style.display = onRRPage ? 'none' : 'flex'; } // --- MODIFIED: The main refresh function to handle all panel states --- function refreshAll() { alertMessageDiv.style.display = 'none'; let showAlert = false, alertText = '', alertBackgroundColor = '', alertBorderColor = ''; if (lossLimit > 0 && totalProfit <= -lossLimit && !alertShownLoss) { showAlert = true; alertText = `🚨 LOSS LIMIT REACHED! -$${lossLimit.toLocaleString()}`; alertBackgroundColor = 'rgba(229, 57, 53, 0.8)'; alertBorderColor = '#E53935'; alertShownLoss = true; localStorage.setItem(ALERT_SHOWN_LOSS_KEY, 'true'); } else if (profitTarget > 0 && totalProfit >= profitTarget && !alertShownProfit) { showAlert = true; alertText = `🎯 PROFIT TARGET REACHED! +$${profitTarget.toLocaleString()}`; alertBackgroundColor = 'rgba(76, 175, 80, 0.8)'; alertBorderColor = '#4CAF50'; alertShownProfit = true; localStorage.setItem(ALERT_SHOWN_PROFIT_KEY, 'true'); } if (showAlert) { alertMessageDiv.textContent = alertText; Object.assign(alertMessageDiv.style, { background: alertBackgroundColor, borderColor: alertBorderColor, display: 'block' }); } const today = new Date(); today.setHours(0, 0, 0, 0); const startOfTodayTimestamp = Math.floor(today.getTime() / 1000); let dailyProfit = 0; for (const game of results) { if (game.timestamp >= startOfTodayTimestamp) { dailyProfit += (game.result === 'win' ? game.bet : -game.bet); } else { break; } } const dailySign = dailyProfit >= 0 ? '+' : '–'; dailyProfitDiv.textContent = `📅 Daily: ${dailySign}$${Math.abs(dailyProfit).toLocaleString()}`; dailyProfitDiv.style.color = dailyProfit >= 0 ? '#4CAF50' : '#E53935'; const sign = totalProfit >= 0 ? '+' : '–'; profitMini.textContent = `${sign}$${Math.abs(totalProfit).toLocaleString()}`; profitMini.style.color = totalProfit >= 0 ? '#4CAF50' : '#E53935'; profitDiv.textContent = `💰 LifeTime Profit: ${sign}$${Math.abs(totalProfit).toLocaleString()}`; profitDiv.style.color = totalProfit >= 0 ? '#4CAF50' : '#E53935'; const wins = results.filter(r => r.result === 'win').length; const tot = results.length; winrateDiv.textContent = `🎯 Win Rate: ${tot ? ((wins / tot) * 100).toFixed(1) : '0.0'}% (${wins}/${tot})`; let w = 0, l = 0; for (const r of results) { if (r.result === 'win') { if (l) break; w++; } else { if (w) break; l++; } } streakDiv.textContent = w ? `🔥 Streak: ${w}` : l ? `💀 Streak: ${l}` : '⏸️ No streak'; resultsContainer.innerHTML = ''; results.slice(0, maxDisplayMatches).forEach((r, i) => { const row = document.createElement('div'); row.append(makeCircle(r.result, r.bet, i), document.createTextNode(`${i + 1}. ${r.result.toUpperCase()} — $${r.bet.toLocaleString()}`)); resultsContainer.appendChild(row); }); if (showEditBetsPanel) { manualEditInputs.forEach((input, i) => { if (manualBets[i] !== undefined) input.value = formatNumberToKMB(manualBets[i]); }); testBetInput.value = formatNumberToKMB(dynamicBetsSettings.testBet); capitalInput.value = formatNumberToKMB(dynamicBetsSettings.capital); startingBetInput.value = formatNumberToKMB(dynamicBetsSettings.startingBet); capitalLock.textContent = dynamicBetsSettings.lockedField === 'capital' ? '🔒' : '🔓'; startingBetLock.textContent = dynamicBetsSettings.lockedField === 'startingBet' ? '🔒' : '🔓'; capitalInput.readOnly = dynamicBetsSettings.lockedField !== 'capital'; startingBetInput.readOnly = dynamicBetsSettings.lockedField !== 'startingBet'; capitalInput.style.background = dynamicBetsSettings.lockedField === 'capital' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.2)'; startingBetInput.style.background = dynamicBetsSettings.lockedField === 'startingBet' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.2)'; } if(showSettings) { profitTargetInput.value = formatNumberToKMB(profitTarget); lossLimitInput.value = formatNumberToKMB(lossLimit); } statsGroup.style.display = 'none'; settingsPanel.style.display = 'none'; betsPanel.style.display = 'none'; editBetsPanel.style.display = 'none'; miniBar.style.display = 'none'; profitMini.style.display = 'none'; buttonContainer.style.display = 'none'; statsDisplayPanel.style.display = 'none'; // Hide all by default if (showSettings) { settingsPanel.style.display = 'flex'; } else if (showBetsPanel) { betsPanel.style.display = 'flex'; } else if (showEditBetsPanel) { editBetsPanel.style.display = 'flex'; } else if (showStatsPanel) { statsDisplayPanel.style.display = 'flex'; // Show stats panel } else if (collapsed) { miniBar.style.display = 'flex'; profitMini.style.display = 'block'; } else { statsGroup.style.display = 'flex'; buttonContainer.style.display = 'flex'; // Show the main button container } buildBetButtons(); updateStatus(); updatePanelVisibility(); updateChargedshootsPanelVisibility(); } const savePos = () => localStorage.setItem(POS_KEY, JSON.stringify({ top: panel.style.top, left: panel.style.left })); function onDragMove(e) { if (!isDragging) return; e.preventDefault(); const moveEvent = e.touches ? e.touches[0] : e; if (typeof moveEvent.clientX === 'undefined') return; const dx = moveEvent.clientX - dragMouseX; const dy = moveEvent.clientY - dragMouseY; dragMouseX = moveEvent.clientX; dragMouseY = moveEvent.clientY; panel.style.left = (panel.offsetLeft + (dx / panelScale)) + 'px'; panel.style.top = (panel.offsetTop + (dy / panelScale)) + 'px'; } function onDragEnd() { if (!isDragging) return; isDragging = false; document.removeEventListener('mousemove', onDragMove); document.removeEventListener('mouseup', onDragEnd); document.removeEventListener('touchmove', onDragMove); document.removeEventListener('touchend', onDragEnd); savePos(); panel.style.cursor = ''; document.body.style.cursor = ''; } function startDrag(e) { if (isTwoFingerDragging) return; e.preventDefault(); const startEvent = e.touches ? e.touches[0] : e; if (typeof startEvent.clientX === 'undefined') return; isDragging = true; dragMouseX = startEvent.clientX; dragMouseY = startEvent.clientY; panel.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing'; document.addEventListener('mousemove', onDragMove); document.addEventListener('mouseup', onDragEnd); document.addEventListener('touchmove', onDragMove, { passive: false }); document.addEventListener('touchend', onDragEnd); } dragHandle.addEventListener('mousedown', startDrag); dragHandle.addEventListener('touchstart', e => { if (e.touches.length === 1) startDrag(e); }); panel.addEventListener('mousedown', e => { if (e.shiftKey && e.target !== dragHandle) startDrag(e); }); panel.addEventListener('touchstart', e => { if (e.touches.length === 2) { e.preventDefault(); isTwoFingerDragging = true; isDragging = false; initialTouchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2; initialTouchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2; initialPanelX = panel.offsetLeft; initialPanelY = panel.offsetTop; panel.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing'; document.addEventListener('touchmove', onTwoFingerMove, { passive: false }); document.addEventListener('touchend', onTwoFingerEnd); } }); function onTwoFingerMove(e) { if (!isTwoFingerDragging || e.touches.length !== 2) { onTwoFingerEnd(e); return; } e.preventDefault(); const currentTouchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2; const currentTouchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2; const dx = currentTouchMidX - initialTouchMidX; const dy = currentTouchMidY - initialTouchMidY; panel.style.left = (initialPanelX + (dx / panelScale)) + 'px'; panel.style.top = (initialPanelY + (dy / panelScale)) + 'px'; } function onTwoFingerEnd(e) { if (!isTwoFingerDragging) return; if (e.touches.length < 2) { document.removeEventListener('touchmove', onTwoFingerMove); document.removeEventListener('touchend', onTwoFingerEnd); savePos(); isTwoFingerDragging = false; panel.style.cursor = ''; document.body.style.cursor = ''; } } const saveChargedshootsPos = () => localStorage.setItem(CHARGED_SHOOTS_POS_KEY, JSON.stringify({ top: chargedshootsPanel.style.top, left: chargedshootsPanel.style.left })); function onChargedPanelDragMove(e) { if (!isChargedPanelDragging) return; e.preventDefault(); const moveEvent = e.touches ? e.touches[0] : e; if (typeof moveEvent.clientX === 'undefined') return; const dx = moveEvent.clientX - dragChargedPanelMouseX; const dy = moveEvent.clientY - dragChargedPanelMouseY; dragChargedPanelMouseX = moveEvent.clientX; dragChargedPanelMouseY = moveEvent.clientY; chargedshootsPanel.style.left = (chargedshootsPanel.offsetLeft + (dx / panelScale)) + 'px'; chargedshootsPanel.style.top = (chargedshootsPanel.offsetTop + (dy / panelScale)) + 'px'; } function onChargedPanelDragEnd() { if (!isChargedPanelDragging) return; isChargedPanelDragging = false; document.removeEventListener('mousemove', onChargedPanelDragMove); document.removeEventListener('mouseup', onChargedPanelDragEnd); document.removeEventListener('touchmove', onChargedPanelDragMove); document.removeEventListener('touchend', onChargedPanelDragEnd); saveChargedshootsPos(); chargedshootsPanel.style.cursor = ''; document.body.style.cursor = ''; } function startChargedPanelDrag(e) { if (isChargedPanelTwoFingerDragging) return; e.preventDefault(); const startEvent = e.touches ? e.touches[0] : e; if (typeof startEvent.clientX === 'undefined') return; isChargedPanelDragging = true; dragChargedPanelMouseX = startEvent.clientX; dragChargedPanelMouseY = startEvent.clientY; chargedshootsPanel.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing'; document.addEventListener('mousemove', onChargedPanelDragMove); document.addEventListener('mouseup', onChargedPanelDragEnd); document.addEventListener('touchmove', onChargedPanelDragMove, { passive: false }); document.addEventListener('touchend', onChargedPanelDragEnd); } chargedshootsPanel.addEventListener('mousedown', e => { if (e.shiftKey) startChargedPanelDrag(e); }); chargedshootsPanel.addEventListener('touchstart', e => { if (e.touches.length === 2) { e.preventDefault(); isChargedPanelTwoFingerDragging = true; isChargedPanelDragging = false; initialChargedPanelTouchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2; initialChargedPanelTouchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2; initialChargedPanelX = chargedshootsPanel.offsetLeft; initialChargedPanelY = chargedshootsPanel.offsetTop; chargedshootsPanel.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing'; document.addEventListener('touchmove', onChargedPanelTwoFingerMove, { passive: false }); document.addEventListener('touchend', onChargedPanelTwoFingerEnd); } }); function onChargedPanelTwoFingerMove(e) { if (!isChargedPanelTwoFingerDragging || e.touches.length !== 2) { onChargedPanelTwoFingerEnd(e); return; } e.preventDefault(); const currentTouchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2; const currentTouchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2; const dx = currentTouchMidX - initialChargedPanelTouchMidX; const dy = currentTouchMidY - initialChargedPanelTouchMidY; chargedshootsPanel.style.left = (initialChargedPanelX + (dx / panelScale)) + 'px'; chargedshootsPanel.style.top = (initialChargedPanelY + (dy / panelScale)) + 'px'; } function onChargedPanelTwoFingerEnd(e) { if (!isChargedPanelTwoFingerDragging) return; if (e.touches.length < 2) { document.removeEventListener('touchmove', onChargedPanelTwoFingerMove); document.removeEventListener('touchend', onChargedPanelTwoFingerEnd); saveChargedshootsPos(); isChargedPanelTwoFingerDragging = false; chargedshootsPanel.style.cursor = ''; document.body.style.cursor = ''; } } // --- MODIFIED: Added showStatsPanel to the collapse logic --- statusDiv.addEventListener('click', () => { if (isDragging || isTwoFingerDragging) return; collapsed = !collapsed; if (collapsed) { showSettings = false; showBetsPanel = false; showEditBetsPanel = false; showStatsPanel = false; } saveCollapsed(); refreshAll(); }); alertMessageDiv.addEventListener('click', () => { alertMessageDiv.style.display = 'none'; }); // --- Main Loop --- calculateBetsAndRefresh(); // Initial calculation and render initiateSyncing(); setInterval(() => { updatePanelVisibility(); updateChargedshootsPanelVisibility(); performAutoShoot(); }, 500); })();