您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a persistent fake BTC balance, live stat tracking with a working graph, and an integrated settings UI to visually simulate gameplay on Stake.com.
// ==UserScript== // @name Stake.com Visual Balance & Live Stats Modifier // @namespace http://tampermonkey.net/ // @version 9.6 // @description Adds a persistent fake BTC balance, live stat tracking with a working graph, and an integrated settings UI to visually simulate gameplay on Stake.com. // @author XaRTeCK (Enhanced by Gemini) // @match *://stake.com/* // @match *://rgs.twist-rgs.com/* // @connect stake.com // @connect rgs.twist-rgs.com // @license CC-BY-NC-ND-4.0 // @grant none // @run-at document-start // ==/UserScript== (function() { 'use strict'; const BALANCE_STORAGE_KEY = 'stake_fake_btc_balance_v4'; const STATS_STORAGE_KEY = 'stake_fake_stats_v3'; const DEFAULT_BTC_VALUE = 0.1; let currentFakeBet = { amount: 0, currency: null }; let fakeStats = getFakeStats(); function getFakeBtcValue() { const savedBalance = localStorage.getItem(BALANCE_STORAGE_KEY); return savedBalance ? parseFloat(savedBalance) : DEFAULT_BTC_VALUE; } // ENHANCED: This function now forces a live UI update without a page refresh. function setFakeBtcValue(amount) { const numericAmount = parseFloat(amount); if (!isNaN(numericAmount) && numericAmount >= 0) { // 1. Save the new balance to local storage localStorage.setItem(BALANCE_STORAGE_KEY, numericAmount.toString()); // 2. Update the footer input if it exists for visual consistency const footerInput = document.getElementById('fake-balance-input-btc'); if (footerInput) { footerInput.value = numericAmount.toFixed(4); } // 3. Force a refresh of the balance display in the UI by simulating a currency switch. const balanceToggle = document.querySelector('[data-testid="balance-toggle"] button'); if (balanceToggle) { // First click opens the currency dropdown balanceToggle.click(); // A small delay ensures the UI has time to react before we click again to close it. // This double action forces the balance component to re-render with the new value. setTimeout(() => balanceToggle.click(), 50); } } } function getFakeStats() { const savedStats = localStorage.getItem(STATS_STORAGE_KEY); const defaultStats = { profit: 0, wagered: 0, wins: 0, losses: 0, profitHistory: [0] }; if (savedStats) { const parsed = JSON.parse(savedStats); if (!Array.isArray(parsed.profitHistory) || parsed.profitHistory.length === 0) { parsed.profitHistory = [0]; } return { ...defaultStats, ...parsed }; } return defaultStats; } function saveFakeStats(stats) { localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(stats)); } function resetFakeStats() { fakeStats = { profit: 0, wagered: 0, wins: 0, losses: 0, profitHistory: [0] }; saveFakeStats(fakeStats); updateLiveStatsDisplay(); } function showBalanceSetupPopup() { const popupHTML = ` <div id="visual-script-welcome" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #2f3c4c; color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.5); z-index: 9999; max-width: 450px; text-align: center; font-family: 'Inter', sans-serif;"> <h2 style="margin: 0 0 15px 0; font-size: 22px;">Visual Gameplay Modifier Active</h2> <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.5; color: #b0bdce;"> Set your visual BTC balance below. You can change this amount anytime at the bottom of the Stake.com page. </p> <div style="display: flex; gap: 10px; align-items: center; margin-bottom: 20px;"> <input type="number" step="0.001" id="popup-fake-balance-input" value="${getFakeBtcValue()}" style="background-color: #1f2a38; border: 1px solid #3c4a5c; color: white; border-radius: 5px; padding: 10px; width: 100%; text-align: center; font-size: 16px;" > <button id="popup-balance-save" style="background-color: #008756; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold; white-space: nowrap;">Set Balance</button> </div> <div style="display: flex; justify-content: flex-end; align-items: center;"> <button id="visual-script-close" style="background-color: #55657e; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold;">Close</button> </div> </div> <div id="visual-script-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9998;"></div> `; document.body.insertAdjacentHTML('beforeend', popupHTML); const closePopup = () => { document.getElementById('visual-script-welcome')?.remove(); document.getElementById('visual-script-overlay')?.remove(); }; document.getElementById('visual-script-close').addEventListener('click', closePopup); document.getElementById('visual-script-overlay').addEventListener('click', closePopup); const saveBtn = document.getElementById('popup-balance-save'); const input = document.getElementById('popup-fake-balance-input'); saveBtn.addEventListener('click', () => { setFakeBtcValue(input.value); saveBtn.textContent = 'Saved!'; saveBtn.style.backgroundColor = '#6CDE07'; setTimeout(() => { saveBtn.textContent = 'Set Balance'; saveBtn.style.backgroundColor = '#008756'; }, 1500); }); } function drawFakeGraph(svg, history) { if (!svg) return; while (svg.firstChild) svg.removeChild(svg.firstChild); if (history.length < 2) return; const width = svg.clientWidth || 225; const height = svg.clientHeight || 170; const padding = 5; const maxProfit = Math.max(...history); const minProfit = Math.min(...history); const range = (maxProfit - minProfit) === 0 ? 1 : maxProfit - minProfit; const getCoords = (value, index) => { const x = (index / (history.length - 1)) * (width - padding * 2) + padding; const y = height - ((value - minProfit) / range) * (height - padding * 2) - padding; return { x: x.toFixed(2), y: y.toFixed(2) }; }; let linePathData = ''; history.forEach((value, index) => { const { x, y } = getCoords(value, index); linePathData += `${index === 0 ? 'M' : 'L'} ${x} ${y} `; }); const lastPoint = getCoords(history[history.length - 1], history.length - 1); const firstPoint = getCoords(history[0], 0); const fillPathData = `${linePathData} L ${lastPoint.x} ${height} L ${firstPoint.x} ${height} Z`; const finalProfit = history[history.length - 1]; const color = finalProfit >= 0 ? 'var(--green-500)' : 'var(--red-500)'; svg.innerHTML = ` <path d="${fillPathData}" fill="${color}" fill-opacity="0.2"></path> <path d="${linePathData}" fill="none" stroke="${color}" stroke-width="2"></path> `; } function updateLiveStatsDisplay() { const profitEl = document.querySelector('[data-testid="bets-stats-profit"]'); if (!profitEl) return; const statsContainer = profitEl.closest('div.draggable'); if (!statsContainer) return; const wageredEl = statsContainer.querySelector('[data-testid="bets-stats-wagered"]'); const winsEl = statsContainer.querySelector('[data-testid="bets-stats-wins"]'); const lossesEl = statsContainer.querySelector('[data-testid="bets-stats-losses"]'); const svg = statsContainer.querySelector('div.graph-wrap svg'); if (wageredEl && winsEl && lossesEl) { const profitValue = fakeStats.profit * 105000; const wageredValue = fakeStats.wagered * 105000; const formatCurrency = (value) => value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }).replace('$', '€'); profitEl.textContent = formatCurrency(profitValue); profitEl.classList.remove('text-positive', 'text-critical'); profitEl.classList.add(profitValue >= 0 ? 'text-positive' : 'text-critical'); wageredEl.textContent = formatCurrency(wageredValue); winsEl.textContent = fakeStats.wins.toLocaleString('en-US'); lossesEl.textContent = fakeStats.losses.toLocaleString('en-US'); if (svg) { drawFakeGraph(svg, fakeStats.profitHistory); } } } function injectBalanceSettings(footerElement) { if (document.getElementById('fake-balance-settings')) return; const settingsHTML = ` <div id="fake-balance-settings" class="p-4 mt-6 border-t-2 border-t-grey-500 text-grey-200"> <div class="flex flex-col gap-2 max-w-sm mx-auto"> <label for="fake-balance-input-btc" class="ds-body-md-strong text-white text-center">Visual Balance Modifier</label> <p class="ds-body-sm text-center">This only changes the balance you see on your screen.</p> <input type="number" step="0.001" id="fake-balance-input-btc" value="${getFakeBtcValue().toFixed(4)}" style="background-color: #1f2a38; border: 1px solid #3c4a5c; color: white; border-radius: 5px; padding: 8px; width: 100%; text-align: center;" > <button id="reset-stats-button-footer" style="background-color: #55657e; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; margin-top: 10px;">Reset Live Stats</button> <span id="fake-balance-saved" style="color: #6CDE07; font-size: 12px; height: 16px; transition: opacity 0.3s ease-out; opacity: 0; text-align: center;">Saved!</span> </div> </div> `; footerElement.insertAdjacentHTML('afterbegin', settingsHTML); const input = document.getElementById('fake-balance-input-btc'); const savedMessage = document.getElementById('fake-balance-saved'); const resetButton = document.getElementById('reset-stats-button-footer'); let timeoutId; input.addEventListener('input', (event) => { setFakeBtcValue(event.target.value); savedMessage.textContent = 'Saved!'; savedMessage.style.opacity = '1'; clearTimeout(timeoutId); timeoutId = setTimeout(() => { savedMessage.style.opacity = '0'; }, 1500); }); resetButton.addEventListener('click', () => { if (confirm('Are you sure you want to reset your visual stats (Profit, Wagered, Wins, Losses, and Graph)?')) { resetFakeStats(); savedMessage.textContent = 'Stats Reset!'; savedMessage.style.opacity = '1'; clearTimeout(timeoutId); timeoutId = setTimeout(() => { savedMessage.style.opacity = '0'; }, 1500); } }); } function updateStatsAndHistory(betAmount, payout) { fakeStats.wagered += betAmount; if (payout > 0) { fakeStats.wins++; fakeStats.profit += payout - betAmount; } else { fakeStats.losses++; fakeStats.profit -= betAmount; } fakeStats.profitHistory.push(fakeStats.profit); saveFakeStats(fakeStats); updateLiveStatsDisplay(); } const originalFetch = window.fetch; window.fetch = async function(url, options) { const FAKE_BTC_BALANCE = getFakeBtcValue(); const FAKE_PROVIDER_BALANCE = FAKE_BTC_BALANCE * 100_000_000; const requestUrl = new URL(url.toString(), window.location.origin); const host = requestUrl.hostname; const path = requestUrl.pathname; if (host.includes('rgs.twist-rgs.com') && path.includes('/wallet/authenticate')) { const response = await originalFetch(url, options); const data = await response.clone().json(); if (data.balance) data.balance.amount = FAKE_PROVIDER_BALANCE; return new Response(JSON.stringify(data), { status: 200, headers: response.headers }); } if (host.includes('stake.com') && path.includes('/_api/graphql') && options?.body) { let requestBody; try { requestBody = JSON.parse(options.body); } catch (e) { return originalFetch(url, options); } let modifiedOptions = options; if (requestBody.operationName === 'UserBalances') { const response = await originalFetch(url, options); const data = await response.clone().json(); const btcBalance = data?.data?.user?.balances.find(b => b.available.currency === 'btc'); if (btcBalance) btcBalance.available.amount = FAKE_BTC_BALANCE; return new Response(JSON.stringify(data), { status: response.status, headers: response.headers }); } if (requestBody.query?.includes('mutation') && requestBody.variables?.amount > 0) { currentFakeBet = { amount: requestBody.variables.amount, currency: requestBody.variables.currency }; const modifiedBody = JSON.parse(JSON.stringify(requestBody)); modifiedBody.variables.amount = 0; modifiedOptions = { ...options, body: JSON.stringify(modifiedBody) }; } const response = await originalFetch(url, modifiedOptions); const responseClone = response.clone(); try { const data = await response.json(); if (data.data) { const gameDataKey = Object.keys(data.data).find(key => data.data[key] && typeof data.data[key] === 'object' && 'amount' in data.data[key]); if (gameDataKey && currentFakeBet.amount > 0) { const gameData = data.data[gameDataKey]; gameData.amount = currentFakeBet.amount; gameData.payout = (gameData.payoutMultiplier || 0) * currentFakeBet.amount; updateStatsAndHistory(currentFakeBet.amount, gameData.payout); if (!gameData.active) currentFakeBet = { amount: 0, currency: null }; return new Response(JSON.stringify(data), { status: 200, headers: response.headers }); } } return responseClone; } catch (e) { return responseClone; } } if (host.includes('stake.com') && path.startsWith('/_api/casino/')) { let modifiedOptions = options; if (/\/(bet|roll|bonus)$/.test(path) && options?.body) { try { const originalRequestBody = JSON.parse(options.body); const modifiedBody = { ...originalRequestBody }; let totalAmount = 0; if (path.includes('/roulette/bet')) { ['colors', 'parities', 'dozens', 'numbers', 'columns', 'halves'].forEach(key => { if (Array.isArray(modifiedBody[key])) modifiedBody[key].forEach(bet => { totalAmount += bet.amount; bet.amount = 0; }); }); } else if (originalRequestBody.amount > 0) { totalAmount = originalRequestBody.amount; modifiedBody.amount = 0; } if (totalAmount > 0) { currentFakeBet = { amount: totalAmount, currency: originalRequestBody.currency }; modifiedOptions = { ...options, body: JSON.stringify(modifiedBody) }; } } catch (e) {} } const response = await originalFetch(url, modifiedOptions); const responseClone = response.clone(); try { const data = await response.json(); const gameDataKey = Object.keys(data).find(key => data[key] && typeof data[key] === 'object' && 'amount' in data[key]); if (gameDataKey && currentFakeBet.amount > 0 && data[gameDataKey].currency === currentFakeBet.currency) { const gameData = data[gameDataKey]; gameData.amount = currentFakeBet.amount; gameData.payout = (gameData.payoutMultiplier || 0) * currentFakeBet.amount; updateStatsAndHistory(currentFakeBet.amount, gameData.payout); if (gameData.state?.rounds) gameData.state.rounds.forEach(r => { if ('amount' in r) r.amount = currentFakeBet.amount; }); if (!gameData.active) currentFakeBet = { amount: 0, currency: null }; return new Response(JSON.stringify(data), { status: 200, headers: response.headers }); } return responseClone; } catch (e) { return responseClone; } } return originalFetch(url, options); }; window.addEventListener('DOMContentLoaded', () => { showBalanceSetupPopup(); const mainObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if (node.matches('footer[data-testid="footer"]') && !document.getElementById('fake-balance-settings')) { injectBalanceSettings(node); } if (node.matches('div.draggable') && node.querySelector('[data-testid="bets-stats-profit"]')) { setTimeout(() => updateLiveStatsDisplay(), 100); } const resetButton = node.matches('[data-testid="draggable-stats-reset"]') ? node : node.querySelector('[data-testid="draggable-stats-reset"]'); if (resetButton && !resetButton.dataset.scriptListenerAttached) { resetButton.dataset.scriptListenerAttached = 'true'; resetButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); if (confirm('Are you sure you want to reset your visual stats? This will clear the graph and all tracked data.')) { resetFakeStats(); } }, true); } } } }); mainObserver.observe(document.body, { childList: true, subtree: true }); }); })();