Result shows immediately when Dealer has 2 cards. Auto-bet waits 2s. Fixed card counting.
// ==UserScript==
// @name Blackjack ToolKit V5.0
// @version 5.0
// @description Result shows immediately when Dealer has 2 cards. Auto-bet waits 2s. Fixed card counting.
// @author M7TEM
// @match https://www.torn.com/page.php?sid=blackjack*
// @match https://www.torn.com/pda.php*step=blackjack*
// @grant GM.xmlHttpRequest
// @connect api.torn.com
// @license MIT
// @namespace blackjack.toolkit
// ==/UserScript==
(function() {
'use strict';
// =========================== PART 1: CONSTANTS ==============================
const PANEL_ID = 'bj-tracker-panel';
const STORAGE = 'torn_bj_tracker_results_v2';
const PROFIT_STORAGE = 'torn_bj_total_profit_v2';
const API_KEY_STORAGE = 'bj_tracker_api_key';
const LAST_SYNC_KEY = 'bj_tracker_last_sync';
const LAST_SCANNED_TIMESTAMP = 'bj_tracker_last_scanned';
const PANEL_POS_KEY = 'bj_tracker_pos';
const SESSION_ACTIVE_KEY = 'bj_session_active';
const SESSION_PROFIT_KEY = 'bj_session_profit';
const SESSION_START_KEY = 'bj_session_start';
const UI_MINIMIZED = 'UI_MINIMIZED';
const RISK_LOCK_KEY = 'bj_risk_lock_target';
const AUTO_BET_KEY = 'bj_auto_bet_settings';
const LOG_ID_WIN = 8355;
const LOG_ID_LOSE = 8354;
const LOG_ID_PUSH = 8358;
const RESULT_LOG_IDS = [LOG_ID_WIN, LOG_ID_LOSE, LOG_ID_PUSH];
const API_SYNC_INTERVAL_MS = 5 * 1000;
// --- State Variables ---
let apiKey = '';
let results = [];
let totalProfit = 0;
let isTrackerDragging = false;
let isSessionActive = false;
let sessionProfit = 0;
let sessionStartDate = 0;
let isSyncing = false;
let isProcessingAction = false;
let currentView = 'main';
let currentStatsTimeframe = 7;
let lastScannedTime = 0;
let isHandActive = false;
let helperUpdateTimeout = null;
let isMinimized = false;
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
let lastRenderedState = '';
let shiftKeyHeld = false;
let lockedParam = localStorage.getItem(RISK_LOCK_KEY) || 'capital';
// Auto-Bet State
let isRoundInProgress = false;
let isDoubleActive = false;
let autoBetTimer = null;
let countdownValue = 0;
let autoBetSettings = { resetWin: false, multLoss: false, multPush: false };
function initializeTrackerState() {
apiKey = localStorage.getItem(API_KEY_STORAGE) || '';
results = JSON.parse(localStorage.getItem(STORAGE) || '[]');
totalProfit = parseFloat(localStorage.getItem(PROFIT_STORAGE) || '0');
isMinimized = localStorage.getItem(UI_MINIMIZED) === 'true';
isSessionActive = JSON.parse(localStorage.getItem(SESSION_ACTIVE_KEY) || 'false');
sessionProfit = parseFloat(localStorage.getItem(SESSION_PROFIT_KEY) || '0');
sessionStartDate = parseInt(localStorage.getItem(SESSION_START_KEY) || '0', 10);
lastScannedTime = parseInt(localStorage.getItem(LAST_SCANNED_TIMESTAMP) || '0', 10);
try {
const savedAuto = JSON.parse(localStorage.getItem(AUTO_BET_KEY));
if(savedAuto) autoBetSettings = savedAuto;
} catch(e){}
if (results.length > 0) {
setTimeout(() => {
results.sort((a,b) => b.timestamp - a.timestamp);
refreshTrackerUI();
}, 0);
}
}
initializeTrackerState();
// --- Utility Functions ---
function formatNumberToKMB(num) {
if (typeof num !== 'number' || !isFinite(num)) return 'NaN';
if (num === 0) return '0';
const abs = Math.abs(num);
const sign = num < 0 ? '-' : '';
if (abs >= 1e9) return sign + (abs / 1e9).toFixed(2) + 'b';
if (abs >= 1e6) return sign + (abs / 1e6).toFixed(2) + 'm';
if (abs >= 1e3) return sign + (abs / 1e3).toFixed(1) + 'k';
return sign + abs.toLocaleString();
}
function safeSetBet(amount) {
const input = document.querySelector('.input-money') || document.querySelector('input.bet');
if (input) {
input.value = String(amount);
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
function safeGetBet() {
const input = document.querySelector('.input-money') || document.querySelector('input.bet');
return input ? parseInt(input.value.replace(/,/g, ''), 10) || 0 : 0;
}
function getCurrentBet() { return safeGetBet(); }
// --- UI Construction ---
function createTrackerPanel() {
if (document.getElementById(PANEL_ID)) return;
const styleSheet = document.createElement("style");
styleSheet.innerText = `
#${PANEL_ID} {
position: fixed; top: 10px; right: 10px; z-index: 1000;
background-color: #1a1a1a; border: 2px solid #FFD700; border-radius: 8px;
padding: 8px; width: 230px; box-shadow: 0 4px 12px rgba(0,0,0,0.5);
font-family: monospace; color: #E0E0E0; line-height: 1.4;
}
.bj-green { color: #4CAF50; font-weight: bold; }
.bj-red { color: #FF6F69; font-weight: bold; }
.bj-grey { color: #AAAAAA; font-weight: normal; }
.bj-gold { color: #FFD700; font-weight: bold; }
.bj-trk-btn {
background: #333; border: 1px solid #555; color: #FFD700; cursor: pointer;
border-radius: 4px; padding: 5px; font-size: 11px; font-weight: bold; text-transform: uppercase;
}
.bj-trk-btn:hover { background: #444; border-color: #FFD700; }
/* Helper Buttons */
.bj-helper-button {
flex-grow: 1; padding: 6px 4px; font-size: 12px; font-weight: 700;
cursor: pointer; color: #E0E0E0; background-color: #2a2a2a;
border: 1px solid #555; border-radius: 4px; text-transform: uppercase;
}
.bj-helper-button:hover { background-color: #444; }
.bj-hit { border-color: #FF5722 !important; background-color: rgba(255, 87, 34, 0.4) !important; color: white !important; }
.bj-stand, .bj-double { border-color: #4CAF50 !important; background-color: rgba(76, 175, 80, 0.4) !important; color: white !important; }
.bj-split { border-color: #2196F3 !important; background-color: rgba(33, 150, 243, 0.4) !important; color: white !important; }
/* Risk Panel & Inputs */
.bj-risk-display {
background: #111; border: 1px solid #333; padding: 5px; margin-top: 5px;
font-size: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
}
.bj-risk-label { color: #888; text-align: left; }
.bj-risk-val { text-align: right; font-weight: bold; }
.bj-input-group { display:flex; align-items:center; gap:2px; }
.bj-input { width: 100%; box-sizing: border-box; background: #333; color: white; border: 1px solid #555; padding: 3px; text-align: right; font-family:monospace;}
.bj-lock-btn { cursor:pointer; font-size:12px; opacity:0.3; user-select:none; }
.bj-lock-btn.locked { opacity:1; color:#4CAF50; }
.bj-input-label { font-size: 10px; color: #aaa; display: block; margin-bottom: 2px; }
/* History */
.bj-history-row {
display: grid; grid-template-columns: 35px 70px 1fr 1fr; gap: 2px;
font-size: 10px; border-bottom: 1px solid #222; padding: 2px 0;
}
.bj-view { display: none; flex-direction: column; gap: 8px; }
.bj-view.active { display: flex; }
`;
document.head.appendChild(styleSheet);
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.style.display = 'flex'; panel.style.flexDirection = 'column'; panel.style.gap = '8px';
document.body.appendChild(panel);
try {
const pos = JSON.parse(localStorage.getItem(PANEL_POS_KEY));
if (pos) { panel.style.top = pos.top; panel.style.left = pos.left; }
} catch(e){}
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'move', borderBottom: '1px solid #333', paddingBottom: '5px' });
header.innerHTML = '<span style="font-weight:bold; color: #FFD700;">♠️ BJ ToolKit V5.0</span>';
const minimizeBtn = document.createElement('span');
minimizeBtn.textContent = isMinimized ? '[+]' : '[-]'; minimizeBtn.style.cursor = 'pointer';
minimizeBtn.onclick = () => {
isMinimized = !isMinimized;
minimizeBtn.textContent = isMinimized ? '[+]' : '[-]';
document.getElementById('bj-trk-content-wrap').style.display = isMinimized ? 'none' : 'block';
localStorage.setItem(UI_MINIMIZED, isMinimized);
};
header.appendChild(minimizeBtn);
panel.appendChild(header);
const contentWrap = document.createElement('div');
contentWrap.id = 'bj-trk-content-wrap';
if(isMinimized) contentWrap.style.display = 'none';
panel.appendChild(contentWrap);
// ================= VIEW 1: MAIN DASHBOARD =================
const mainView = document.createElement('div');
mainView.id = 'bj-view-main';
mainView.className = 'bj-view active';
contentWrap.appendChild(mainView);
mainView.innerHTML = `
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:5px; font-size:12px;">
<div id="bj-total-profit">Life: <span class="bj-grey">...</span></div>
<div id="bj-daily-profit">Daily: <span class="bj-grey">...</span></div>
</div>
<div id="bj-session-profit" style="font-size:12px;">Session: <span class="bj-grey">...</span></div>
<div id="bj-sync-status" style="font-size: 9px; color: #666; margin-top:2px;">Sync: --</div>
`;
const actionRow = document.createElement('div');
Object.assign(actionRow.style, { display: 'flex', gap: '5px' });
actionRow.innerHTML = `
<button id="nav-session" class="bj-trk-btn" style="flex:1">▶</button>
<button id="nav-stats" class="bj-trk-btn" style="flex:1">📊</button>
<button id="nav-settings" class="bj-trk-btn" style="flex:1">⚙️</button>
<button id="nav-sync" class="bj-trk-btn" style="flex:1">🔄</button>
`;
mainView.appendChild(actionRow);
const riskDisplay = document.createElement('div');
riskDisplay.className = 'bj-risk-display';
riskDisplay.innerHTML = `
<span class="bj-risk-label">Streak:</span> <span id="rk-streak" class="bj-risk-val">0</span>
<span class="bj-risk-label">Total Loss:</span> <span id="rk-tot-loss" class="bj-risk-val" style="color:#FF6F69">$0</span>
<span class="bj-risk-label">Pot. Loss:</span> <span id="rk-pot-loss" class="bj-risk-val" style="color:#FF6F69">$0</span>
<span class="bj-risk-label">Pot. Profit:</span> <span id="rk-pot-prof" class="bj-risk-val" style="color:#4CAF50">$0</span>
`;
mainView.appendChild(riskDisplay);
const bettingControls = document.createElement('div');
bettingControls.id = 'bj-betting-controls';
mainView.appendChild(bettingControls);
const dynamicContent = document.createElement('div');
dynamicContent.id = 'bj-dynamic-content';
dynamicContent.innerHTML = `<div id="bj-recent-games" style="max-height: 120px; overflow-y: auto; border: 1px solid #333; padding: 2px; background: rgba(0,0,0,0.3);"></div>`;
mainView.appendChild(dynamicContent);
// ================= VIEW 2: SETTINGS PAGE =================
const settingsView = document.createElement('div');
settingsView.id = 'bj-view-settings';
settingsView.className = 'bj-view';
settingsView.innerHTML = `
<button class="bj-trk-btn" id="sets-back" style="width:100%">« Back to Game</button>
<div style="background:#111; padding:5px; border:1px solid #333;">
<h4 style="margin:0 0 5px 0; color:#FFD700; font-size:11px;">Risk Calculator</h4>
<div style="display:flex; flex-direction:column; gap:5px;">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:5px;">
<div><span class="bj-input-label">Streak</span><input id="bj-risk-streak" class="bj-input" value="${localStorage.getItem('bj_risk_streak')||'10'}"></div>
<div><span class="bj-input-label">Mult (x)</span><input id="bj-multiplier-input" class="bj-input" value="${localStorage.getItem('bj_multiplier_value')||'2.2'}" step="0.1"></div>
</div>
<div>
<span class="bj-input-label">Total Capital</span>
<div class="bj-input-group">
<span class="bj-lock-btn" id="lock-cap" title="Lock Capital">🔒</span>
<input id="bj-risk-capital" class="bj-input" value="${localStorage.getItem('bj_risk_capital')||'10000000'}">
</div>
</div>
<div>
<span class="bj-input-label">Starting Bet</span>
<div class="bj-input-group">
<span class="bj-lock-btn" id="lock-bet" title="Lock Bet">🔒</span>
<input id="bj-start-bet-input" class="bj-input" value="${localStorage.getItem('bj_start_bet_value')||'20000'}">
</div>
</div>
</div>
</div>
<div style="background:#111; padding:5px; border:1px solid #333;">
<h4 style="margin:0 0 5px 0; color:#FFD700; font-size:11px;">Auto-Betting (2s Delay)</h4>
<div style="display:flex; flex-direction:column; gap:4px;">
<label style="font-size:11px; cursor:pointer;"><input type="checkbox" id="ab-reset-win"> Reset after Win</label>
<label style="font-size:11px; cursor:pointer;"><input type="checkbox" id="ab-mult-loss"> Multiply after Loss</label>
<label style="font-size:11px; cursor:pointer;"><input type="checkbox" id="ab-mult-push"> Multiply after Push</label>
</div>
</div>
<div style="background:#111; padding:5px; border:1px solid #333;">
<h4 style="margin:0 0 5px 0; color:#FFD700; font-size:11px;">API Data</h4>
<input type="password" id="bj-api-input" class="bj-input" value="${apiKey}" placeholder="API Key">
<div style="display:flex; gap:5px; margin-top:5px;">
<button id="bj-save-api" class="bj-trk-btn" style="flex:1">Save</button>
<button id="bj-reset-data" class="bj-trk-btn" style="flex:1; color:tomato; border-color:tomato">Reset</button>
</div>
</div>
`;
contentWrap.appendChild(settingsView);
// ================= VIEW 3: STATS PAGE =================
const statsView = document.createElement('div');
statsView.id = 'bj-view-stats';
statsView.className = 'bj-view';
statsView.innerHTML = `
<button class="bj-trk-btn" id="stats-back" style="width:100%">« Back to Game</button>
<div style="background:#1a1a1a; padding:5px; border:1px solid #444;">
<canvas id="bj-graph-canvas" width="200" height="150"></canvas>
<div style="display:flex; gap:2px; justify-content:center; margin-top:5px;">
<button class="bj-trk-btn" data-days="1">1D</button>
<button class="bj-trk-btn" data-days="7">7D</button>
<button class="bj-trk-btn" data-days="30">30D</button>
<button class="bj-trk-btn" data-days="365">All</button>
</div>
</div>
<div id="bj-winrate" style="text-align:center; font-size:10px; margin-top:5px; color:#aaa;"></div>
`;
contentWrap.appendChild(statsView);
// --- Event Wiring ---
document.getElementById('nav-session').onclick = toggleSession;
document.getElementById('nav-sync').onclick = () => importApiData(false);
document.getElementById('nav-settings').onclick = () => switchView('settings');
document.getElementById('nav-stats').onclick = () => switchView('stats');
document.getElementById('sets-back').onclick = () => switchView('main');
document.getElementById('stats-back').onclick = () => switchView('main');
statsView.querySelectorAll('[data-days]').forEach(btn => {
btn.onclick = () => { currentStatsTimeframe = parseInt(btn.getAttribute('data-days')); updateGraph(); };
});
setupSettingsEvents();
setupPersistentControls();
// Drag Logic
header.onmousedown = function(e) {
isTrackerDragging = true;
const offsetX = e.clientX - panel.offsetLeft;
const offsetY = e.clientY - panel.offsetTop;
function mouseMove(e) {
if (!isTrackerDragging) return;
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
const r = panel.getBoundingClientRect();
newX = Math.max(0, Math.min(newX, window.innerWidth - r.width));
newY = Math.max(0, Math.min(newY, window.innerHeight - r.height));
panel.style.left = newX + 'px'; panel.style.top = newY + 'px';
}
function mouseUp() {
isTrackerDragging = false;
localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ top: panel.style.top, left: panel.style.left }));
document.removeEventListener('mousemove', mouseMove);
document.removeEventListener('mouseup', mouseUp);
}
document.addEventListener('mousemove', mouseMove);
document.addEventListener('mouseup', mouseUp);
};
}
function switchView(viewName) {
currentView = viewName;
document.querySelectorAll('.bj-view').forEach(el => el.classList.remove('active'));
document.getElementById(`bj-view-${viewName}`).classList.add('active');
refreshTrackerUI();
if(viewName === 'stats') setTimeout(updateGraph, 50);
}
function setupSettingsEvents() {
document.getElementById('bj-save-api').onclick = () => {
apiKey = document.getElementById('bj-api-input').value.trim();
localStorage.setItem(API_KEY_STORAGE, apiKey);
localStorage.setItem(LAST_SCANNED_TIMESTAMP, '0');
lastScannedTime = 0;
importApiData(false);
};
document.getElementById('bj-reset-data').onclick = () => {
if(confirm("Reset all data?")) { localStorage.clear(); location.reload(); }
};
const elCap = document.getElementById('bj-risk-capital');
const elStreak = document.getElementById('bj-risk-streak');
const elMult = document.getElementById('bj-multiplier-input');
const elBet = document.getElementById('bj-start-bet-input');
const lCap = document.getElementById('lock-cap');
const lBet = document.getElementById('lock-bet');
// Auto-Bet Checkboxes
const abReset = document.getElementById('ab-reset-win');
const abMultLoss = document.getElementById('ab-mult-loss');
const abMultPush = document.getElementById('ab-mult-push');
abReset.checked = autoBetSettings.resetWin;
abMultLoss.checked = autoBetSettings.multLoss;
abMultPush.checked = autoBetSettings.multPush;
const saveAutoBet = () => {
autoBetSettings = { resetWin: abReset.checked, multLoss: abMultLoss.checked, multPush: abMultPush.checked };
localStorage.setItem(AUTO_BET_KEY, JSON.stringify(autoBetSettings));
};
abReset.onchange = saveAutoBet;
abMultLoss.onchange = saveAutoBet;
abMultPush.onchange = saveAutoBet;
const clean = (val) => parseFloat(String(val).replace(/,/g, '')) || 0;
function updateLockUI() {
lCap.classList.toggle('locked', lockedParam === 'capital');
lBet.classList.toggle('locked', lockedParam === 'bet');
elCap.style.borderColor = lockedParam === 'capital' ? '#4CAF50' : '#555';
elBet.style.borderColor = lockedParam === 'bet' ? '#4CAF50' : '#555';
}
updateLockUI();
lCap.onclick = () => { lockedParam = 'capital'; localStorage.setItem(RISK_LOCK_KEY, lockedParam); updateLockUI(); };
lBet.onclick = () => { lockedParam = 'bet'; localStorage.setItem(RISK_LOCK_KEY, lockedParam); updateLockUI(); };
function calculateRisk(source) {
const streak = clean(elStreak.value);
const mult = clean(elMult.value);
if (mult <= 1 || streak <= 0) return;
const factor = (1 - Math.pow(mult, streak)) / (1 - mult);
if (source === 'capital') {
lockedParam = 'capital';
const cap = clean(elCap.value);
const newBet = Math.floor(cap / factor);
elBet.value = newBet;
localStorage.setItem('bj_start_bet_value', newBet);
localStorage.setItem('bj_risk_capital', cap);
}
else if (source === 'bet') {
lockedParam = 'bet';
const bet = clean(elBet.value);
const newCap = Math.ceil(bet * factor);
elCap.value = newCap;
localStorage.setItem('bj_risk_capital', newCap);
localStorage.setItem('bj_start_bet_value', bet);
}
else if (source === 'calc') {
if (lockedParam === 'capital') {
const cap = clean(elCap.value);
const newBet = Math.floor(cap / factor);
elBet.value = newBet;
localStorage.setItem('bj_start_bet_value', newBet);
} else {
const bet = clean(elBet.value);
const newCap = Math.ceil(bet * factor);
elCap.value = newCap;
localStorage.setItem('bj_risk_capital', newCap);
}
}
localStorage.setItem(RISK_LOCK_KEY, lockedParam);
localStorage.setItem('bj_risk_streak', streak);
localStorage.setItem('bj_multiplier_value', mult);
updateLockUI();
}
elCap.addEventListener('keyup', () => calculateRisk('capital'));
elBet.addEventListener('keyup', () => calculateRisk('bet'));
elStreak.addEventListener('keyup', () => calculateRisk('calc'));
elMult.addEventListener('keyup', () => calculateRisk('calc'));
}
function setupPersistentControls() {
const controls = document.getElementById('bj-betting-controls');
if(!controls) return;
controls.innerHTML = '';
const row = document.createElement('div');
row.style.display = 'flex'; row.style.gap = '5px'; row.style.marginTop = '5px';
const btnStart = document.createElement('button'); btnStart.textContent = 'Deal'; btnStart.className = 'bj-helper-button'; btnStart.style.backgroundColor = '#F9A825'; btnStart.style.color='#333';
const btnReset = document.createElement('button'); btnReset.textContent = 'Reset'; btnReset.className = 'bj-helper-button';
const btnMult = document.createElement('button'); btnMult.id = 'bj-ctrl-mult'; btnMult.className = 'bj-helper-button';
const btnDiv = document.createElement('button'); btnDiv.id = 'bj-ctrl-div'; btnDiv.className = 'bj-helper-button';
const mult = parseFloat(localStorage.getItem('bj_multiplier_value') || '2.2').toFixed(1);
btnMult.textContent = `x${mult}`; btnDiv.textContent = `/${mult}`;
row.append(btnStart, btnReset, btnMult, btnDiv);
controls.appendChild(row);
setupHelperButtonEvents(btnStart, btnReset, btnMult, btnDiv, null, null, null, null);
}
function renderHelperContent(advice, handInfo, actionColor) {
const container = document.createElement('div');
Object.assign(container.style, { padding: '5px', background: '#1a1a1a', border: '1px solid #444', borderRadius: '4px', display:'flex', flexDirection:'column', gap:'5px'});
container.innerHTML = `
<div class="bj-trk-advice" style="--bj-border-color:${actionColor}">${advice}</div>
<div id="bj-helper-hand-total" style="font-size:12px; text-align:center; color:#ccc;">${handInfo}</div>
`;
const row = document.createElement('div');
row.style.display = 'flex'; row.style.gap = '5px';
const bHit = document.createElement('button'); bHit.textContent='HIT'; bHit.className='bj-helper-button';
const bStd = document.createElement('button'); bStd.textContent='STAND'; bStd.className='bj-helper-button';
const bDbl = document.createElement('button'); bDbl.textContent='DBL'; bDbl.className='bj-helper-button';
const bSpl = document.createElement('button'); bSpl.textContent='SPLIT'; bSpl.className='bj-helper-button';
row.append(bHit, bStd, bDbl, bSpl);
container.appendChild(row);
bHit.style.display = 'none';
bStd.style.display = 'none';
bDbl.style.display = 'none';
bSpl.style.display = 'none';
let visibleBtn = null;
if(advice === 'Hit') { bHit.style.display = 'block'; bHit.classList.add('bj-hit'); visibleBtn = bHit; }
else if(advice === 'Stand') { bStd.style.display = 'block'; bStd.classList.add('bj-stand'); visibleBtn = bStd; }
else if(advice === 'Double') { bDbl.style.display = 'block'; bDbl.classList.add('bj-double'); visibleBtn = bDbl; }
else if(advice === 'Split') { bSpl.style.display = 'block'; bSpl.classList.add('bj-split'); visibleBtn = bSpl; }
if (visibleBtn) {
visibleBtn.style.width = '100%';
visibleBtn.style.padding = '10px';
visibleBtn.style.fontSize = '14px';
}
setupHelperButtonEvents(null,null,null,null, bHit, bStd, bDbl, bSpl);
return container;
}
function updateRiskDisplay() {
const elStreak = document.getElementById('rk-streak');
const elTotLoss = document.getElementById('rk-tot-loss');
const elPotLoss = document.getElementById('rk-pot-loss');
const elPotProf = document.getElementById('rk-pot-prof');
if (!elStreak) return;
const sorted = results.slice().sort((a,b) => b.timestamp - a.timestamp);
let streak = 0;
let totalLoss = 0;
for (let r of sorted) {
if (r.result === 'lose') { streak++; totalLoss += Math.abs(r.profit); }
else if (r.result === 'push') { continue; }
else { break; }
}
const currentBet = getCurrentBet();
const potentialLoss = totalLoss + currentBet;
const potentialProfit = currentBet - totalLoss;
elStreak.textContent = streak;
elTotLoss.textContent = '$' + formatNumberToKMB(totalLoss);
elPotLoss.textContent = '$' + formatNumberToKMB(potentialLoss);
elPotProf.textContent = (potentialProfit >= 0 ? '+' : '-') + '$' + formatNumberToKMB(Math.abs(potentialProfit));
elPotProf.style.color = potentialProfit >= 0 ? '#4CAF50' : '#FF6F69';
}
function renderRecentGamesList() {
const container = document.getElementById('bj-recent-games');
if (!container) return;
let chronological = results.slice().sort((a,b) => a.timestamp - b.timestamp);
let runningTotal = 0;
const processed = chronological.map(r => {
runningTotal += r.profit;
return { ...r, accumulated: runningTotal };
});
const display = processed.sort((a,b) => b.timestamp - a.timestamp).slice(0, 100);
let html = '';
display.forEach((r, idx) => {
let color = '#888'; let sign = '';
let outcome = r.result.toUpperCase();
if (r.isNatural) outcome = 'NATURAL';
else if (r.isDouble && r.result==='win') outcome = 'WIN DBL';
else if (r.isDouble && r.result==='lose') outcome = 'LOSE DBL';
if (r.result === 'win') { color = '#4CAF50'; sign = '+'; }
else if (r.result === 'lose') { color = '#E53935'; sign = '-'; }
else if (r.result === 'push') { color = '#FFC107'; sign = ''; }
let accColor = r.accumulated >= 0 ? '#4CAF50' : '#E53935';
html += `
<div class="bj-history-row">
<span style="color:#666; text-align:left;">#${results.length - idx}</span>
<span style="color:${color}; text-align:left; font-weight:bold; white-space:nowrap; overflow:hidden;">${outcome}</span>
<span style="color:${color}; text-align:right;">${sign}${formatNumberToKMB(Math.abs(r.profit))}</span>
<span style="color:${accColor}; text-align:right;">${formatNumberToKMB(r.accumulated)}</span>
</div>`;
});
container.innerHTML = html;
}
function setupHelperButtonEvents(btnStart, btnResetBet, btnMult, btnDiv, btnHit, btnStand, btnDouble, btnSplit) {
if (typeof isProcessingAction === 'undefined') window.isProcessingAction = false;
const clickAction = (step, confirm) => {
if (window.isProcessingAction) return;
window.isProcessingAction = true;
document.querySelectorAll('.bj-helper-button').forEach(b => b.style.opacity = '0.5');
// --- TRACK DOUBLE DOWN ---
if (step === 'doubleDown') isDoubleActive = true;
else if (step === 'hit' || step === 'stand' || step === 'split') isDoubleActive = false;
const area = document.querySelector(`[data-step="${step}"]`);
if(area) {
area.dispatchEvent(new MouseEvent('click', {bubbles:true}));
if(confirm) setTimeout(() => {
const yes = document.querySelector('.confirm-action.yes') || document.querySelector('.action-yes');
if(yes) yes.click();
}, 100);
}
setTimeout(() => { window.isProcessingAction = false; document.querySelectorAll('.bj-helper-button').forEach(b => b.style.opacity = '1'); }, 200);
};
if(btnHit) btnHit.onclick = () => clickAction('hit');
if(btnStand) btnStand.onclick = () => clickAction('stand');
if(btnDouble) btnDouble.onclick = () => clickAction('doubleDown', true);
if(btnSplit) btnSplit.onclick = () => clickAction('split', true);
if(btnStart) btnStart.onclick = () => {
const btn = document.querySelector('[data-step="startGame"]');
if(btn) {
isDoubleActive = false; // Reset
btn.click();
setTimeout(() => {
const yes = document.querySelector('.bet-confirm .yes') || document.querySelector('.confirm-yes');
if(yes) yes.click();
}, 100);
}
};
if(btnResetBet) btnResetBet.onclick = () => {
const val = document.getElementById('bj-start-bet-input').value;
safeSetBet(parseInt(val.replace(/,/g,''),10));
};
if(btnMult) btnMult.onclick = () => {
const mult = parseFloat(document.getElementById('bj-multiplier-input').value) || 2.2;
safeSetBet(Math.round(safeGetBet() * mult));
};
if(btnDiv) btnDiv.onclick = () => {
const mult = parseFloat(document.getElementById('bj-multiplier-input').value) || 2.2;
safeSetBet(Math.max(100, Math.round(safeGetBet() / mult)));
};
}
function refreshTrackerUI() {
if(currentView === 'main') {
const totalEl = document.getElementById('bj-total-profit');
if (totalEl) totalEl.innerHTML = `Life: <span class="${totalProfit >= 0 ? 'bj-green' : 'bj-red'}">$${formatNumberToKMB(totalProfit)}</span>`;
const sessionEl = document.getElementById('bj-session-profit');
if (sessionEl) sessionEl.innerHTML = `Session: <span class="${isSessionActive ? (sessionProfit >= 0 ? 'bj-green' : 'bj-red') : 'bj-grey'}">${isSessionActive ? '$' + formatNumberToKMB(sessionProfit) : 'Inactive'}</span>`;
const today = new Date();
today.setHours(0,0,0,0);
const startDay = Math.floor(today.getTime()/1000);
let dailyProfit = results.filter(r => r.timestamp >= startDay).reduce((acc, r) => acc + r.profit, 0);
const dailyEl = document.getElementById('bj-daily-profit');
if (dailyEl) dailyEl.innerHTML = `Daily: <span class="${dailyProfit >= 0 ? 'bj-green' : 'bj-red'}">$${formatNumberToKMB(dailyProfit)}</span>`;
const btnSess = document.getElementById('nav-session');
if(btnSess) btnSess.textContent = isSessionActive ? '⏹' : '▶';
const status = document.getElementById('bj-sync-status');
if (status) {
const next = new Date(Date.now() + API_SYNC_INTERVAL_MS).toLocaleTimeString();
status.textContent = `${isSyncing?'Syncing...':(lastScannedTime>1?'Scanned':'Partial')} | Next: ${next}`;
}
const mult = parseFloat(localStorage.getItem('bj_multiplier_value') || '2.2').toFixed(1);
const bm = document.getElementById('bj-ctrl-mult'); if(bm) bm.textContent=`x${mult}`;
const bd = document.getElementById('bj-ctrl-div'); if(bd) bd.textContent=`/${mult}`;
updateRiskDisplay();
renderRecentGamesList();
}
else if (currentView === 'stats') {
const wins = results.filter(r => r.result === 'win').length;
const losses = results.filter(r => r.result === 'lose').length;
const pushes = results.filter(r => r.result === 'push').length;
const wr = document.getElementById('bj-winrate');
if(wr) wr.innerHTML = `<span class="bj-green">W:${wins}</span> <span class="bj-red">L:${losses}</span> <span class="bj-gold">P:${pushes}</span>`;
}
}
function toggleSession() {
isSessionActive = !isSessionActive;
if (isSessionActive) {
sessionProfit = 0;
sessionStartDate = Date.now();
sessionProfit = results.filter(r => r.timestamp * 1000 >= sessionStartDate).reduce((sum, r) => sum + r.profit, 0);
} else {
sessionStartDate = 0;
}
localStorage.setItem(SESSION_ACTIVE_KEY, JSON.stringify(isSessionActive));
localStorage.setItem(SESSION_PROFIT_KEY, sessionProfit.toString());
localStorage.setItem(SESSION_START_KEY, sessionStartDate.toString());
refreshTrackerUI();
}
async function importApiData(silent = true) {
if(isSyncing || !apiKey) return; isSyncing = true;
try {
const forward = await fetchLogs((results[0]?.timestamp || (Date.now()/1000)-86400));
if(forward.length) processLogs(forward);
if(!lastScannedTime) {
let scanning=true, pt=results[results.length-1]?.timestamp || Math.floor(Date.now()/1000);
while(scanning) {
const back = await fetchLogs(null, pt);
if(back.length) {
processLogs(back);
pt = Math.min(...back.map(l=>l.timestamp))-1;
if(back.length<100) { scanning=false; lastScannedTime=1; }
} else { scanning=false; lastScannedTime=1; }
}
localStorage.setItem(LAST_SCANNED_TIMESTAMP, lastScannedTime);
}
totalProfit = results.reduce((s,r)=>s+r.profit,0);
sessionProfit = results.filter(r=>r.timestamp*1000 >= sessionStartDate).reduce((s,r)=>s+r.profit,0);
localStorage.setItem(STORAGE, JSON.stringify(results));
localStorage.setItem(PROFIT_STORAGE, totalProfit);
localStorage.setItem(SESSION_PROFIT_KEY, sessionProfit);
refreshTrackerUI();
if(currentView === 'stats') updateGraph();
} catch(e) { console.error(e); } finally { isSyncing=false; refreshTrackerUI(); }
}
async function fetchLogs(from, to) {
let url = `https://api.torn.com/user/?selections=log&log=${RESULT_LOG_IDS.join(',')}&key=${apiKey}`;
if(from) url+=`&from=${from+1}`; if(to) url+=`&to=${to}`;
return new Promise(res => GM.xmlHttpRequest({method:"GET", url, onload:r=>{
try{ res(Object.values(JSON.parse(r.responseText).log||{})); }catch{res([]);}
}}));
}
function processLogs(logs) {
const exist = new Set(results.map(r=>r.timestamp));
for(let l of logs) {
if(exist.has(l.timestamp)) continue;
let res, prof=0, isNat=(l.data.win_state||'').includes('natural');
if(l.log===LOG_ID_WIN) { prof=l.data.winnings-(l.data.winnings/(isNat?2.5:2)); res='win'; }
else if(l.log===LOG_ID_LOSE) { prof=-(l.data.losses||0); res='lose'; }
else if(l.log===LOG_ID_PUSH) { res='push'; }
if(res) results.push({result:res, profit:prof, timestamp:l.timestamp, isNatural:isNat, isDouble: (l.data.winnings/(isNat?2.5:2)) > 10000000 });
}
results.sort((a,b)=>b.timestamp-a.timestamp);
}
function updateGraph() {
const c = document.getElementById('bj-graph-canvas'); if(!c) return;
const ctx = c.getContext('2d'); ctx.clearRect(0,0,c.width,c.height);
const pts = results.filter(r=>r.timestamp >= (Date.now()/1000)-(currentStatsTimeframe*86400)).sort((a,b)=>a.timestamp-b.timestamp);
if(pts.length<2) return;
let acc=0; const data=pts.map(p=>{acc+=p.profit; return {t:p.timestamp, v:acc};});
const p=20, W=c.width-p*2, H=c.height-p*2;
const minV=Math.min(0,...data.map(d=>d.v)), maxV=Math.max(0,...data.map(d=>d.v));
const minT=data[0].t, rangeT=data[data.length-1].t-minT, rangeV=maxV-minV||1;
ctx.strokeStyle='#444'; const zY = (c.height-p) - ((0-minV)/rangeV)*H;
ctx.beginPath(); ctx.moveTo(p, zY); ctx.lineTo(c.width-p, zY); ctx.stroke();
ctx.strokeStyle='#FFD700'; ctx.lineWidth=1.5; ctx.beginPath();
data.forEach((d,i)=>{
const x = p + ((d.t-minT)/rangeT)*W;
const y = (c.height-p) - ((d.v-minV)/rangeV)*H;
if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
});
ctx.stroke();
}
function updateHelper() {
if(currentView !== 'main') return;
const dc = document.getElementById('bj-dynamic-content'); if(!dc) return;
const panel = document.getElementById(PANEL_ID);
const pHand = getHandInfo('.player-cards');
const dHand = getHandInfo('.dealer-cards');
// --- STRICT LOGIC: Wait for 2 dealer cards (unless player busted) ---
let gameState = 'waiting';
if (pHand.cards.length >= 2) {
if (pHand.total > 21) {
// Player Bust -> Immediate End
gameState = 'over';
} else if (dHand.cards.length === 1) {
// Dealer 1 Card -> Active Round (Strategy)
gameState = 'active';
} else if (dHand.cards.length >= 2) {
// Dealer 2+ Cards -> Round Over (Result)
gameState = 'over';
}
}
updateRiskDisplay();
const stateKey = `${gameState}_${pHand.total}_${dHand.total}_${results.length}`;
// If timer is running, allow re-render for countdown text only
if(stateKey === lastRenderedState && !autoBetTimer) return;
lastRenderedState = stateKey;
if (gameState === 'active') {
// --- ACTIVE: SHOW STRATEGY ---
isRoundInProgress = true;
if (autoBetTimer) { clearInterval(autoBetTimer); autoBetTimer = null; }
const dVal = dHand.cards[0]===11?1:dHand.cards[0];
let strat = getStrategyAction(pHand, dVal);
if (pHand.total === 21) strat = 'Stand';
const color = strat.includes('Hit')?'#FF5722':(strat.includes('Stand')?'#4CAF50':'#2196F3');
const pTxt = `[${pHand.cardTexts.join(',')}]`;
const dTxt = `[${dHand.cardTexts.join(',')}]`;
dc.innerHTML=''; dc.appendChild(renderHelperContent(strat, `P: ${pHand.total} ${pTxt} | D: ${dHand.total} ${dTxt}`, color));
panel.style.border = `2px solid ${color}`;
} else if (gameState === 'over') {
// --- OVER: RESULT & COUNTDOWN ---
panel.style.border = '2px solid #FFD700';
let resText = "Processing Result...", resColor = "#888";
let outcome = null;
const pTxt = `[${pHand.cardTexts.join(',')}]`;
const dTxt = `[${dHand.cardTexts.join(',')}]`;
if(pHand.total > 0) {
if(pHand.total>21) {
resText = `YOU LOSE (BUST ${pHand.total} ${pTxt})`; resColor="#E53935"; outcome='lose';
}
else if(dHand.total>21) {
resText = `YOU WIN (DEALER BUST ${dHand.total} ${dTxt})`; resColor="#4CAF50"; outcome='win';
}
else if(pHand.total>dHand.total) {
resText = `YOU WIN (${pHand.total} ${pTxt} > ${dHand.total} ${dTxt})`; resColor="#4CAF50"; outcome='win';
}
else if(dHand.total>pHand.total) {
resText = `YOU LOSE (DEALER ${dHand.total} ${dTxt} > YOU ${pHand.total} ${pTxt})`; resColor="#E53935"; outcome='lose';
}
else {
resText=`PUSH (${pHand.total} ${pTxt} = ${dHand.total} ${dTxt})`; resColor="#FFC107"; outcome='push';
}
}
// --- IMMEDIATE VISUAL UPDATE ---
// Update the result text regardless of the timer status
// This ensures "YOU WIN" is seen immediately.
const timerHtml = autoBetTimer ? `<br><span style="color:#FFD700; font-size:10px; font-weight:normal; display:block; margin-top:3px;">Auto-betting in ${countdownValue}s...</span>` : '';
dc.innerHTML = `<div id="bj-result-message" style="text-align:center; padding:5px; font-weight:bold; color:${resColor}; font-size:11px; border-bottom:1px solid #333; text-transform:uppercase;">${resText}${timerHtml}</div><div id="bj-recent-games" style="max-height:120px; overflow-y:auto; border:1px solid #333; padding:2px; background:rgba(0,0,0,0.3);"></div>`;
renderRecentGamesList();
// --- AUTO BET TRIGGER ---
// Only start timer if round WAS in progress and NOT already counting down
if (isRoundInProgress && outcome && !autoBetTimer) {
isRoundInProgress = false;
const mult = parseFloat(document.getElementById('bj-multiplier-input').value) || 2.0;
const startBetVal = parseInt(document.getElementById('bj-start-bet-input').value.replace(/,/g, '')) || 0;
const shouldBet = (outcome === 'win' && autoBetSettings.resetWin) ||
(outcome === 'lose' && autoBetSettings.multLoss) ||
(outcome === 'push' && autoBetSettings.multPush);
if (shouldBet) {
countdownValue = 2;
// Re-render to show the initial "in 2s"
const el = document.getElementById('bj-result-message');
if (el) el.innerHTML = `${resText}<br><span style="color:#FFD700; font-size:10px; font-weight:normal; display:block; margin-top:3px;">Auto-betting in ${countdownValue}s...</span>`;
autoBetTimer = setInterval(() => {
countdownValue--;
const elUpdate = document.getElementById('bj-result-message');
// Update visuals every second
if (elUpdate) elUpdate.innerHTML = `${resText}<br><span style="color:#FFD700; font-size:10px; font-weight:normal; display:block; margin-top:3px;">Auto-betting in ${countdownValue}s...</span>`;
if (countdownValue <= 0) {
clearInterval(autoBetTimer);
autoBetTimer = null;
if (outcome === 'win') {
safeSetBet(startBetVal);
}
else if (outcome === 'lose') {
if (isDoubleActive) safeSetBet(Math.round(safeGetBet() * mult * mult));
else safeSetBet(Math.round(safeGetBet() * mult));
}
else if (outcome === 'push') {
safeSetBet(Math.round(safeGetBet() * mult));
}
// Clean up the text after betting
if (elUpdate) elUpdate.innerHTML = resText;
}
}, 1000);
}
}
} else {
// --- WAITING: 0 Dealer Cards ---
panel.style.border = '2px solid #FFD700';
dc.innerHTML = `<div id="bj-result-message" style="text-align:center; padding:5px; font-weight:bold; color:#888; font-size:12px; border-bottom:1px solid #333;">Waiting for Deal...</div><div id="bj-recent-games" style="max-height:120px; overflow-y:auto; border:1px solid #333; padding:2px; background:rgba(0,0,0,0.3);"></div>`;
renderRecentGamesList();
}
}
function getStrategyAction(p, d) {
const dIdx = d===1?11:d;
let action = 'Hit';
if(p.isPair && p.cards.length === 2) {
const v = p.cards[0]===11?11:p.cards[0];
if(v===11||v===8) action = 'Split';
else if(v===5) action = (dIdx<10)?'Double':'Hit';
else if(v===10) action = 'Stand';
else if(v===9) action = (dIdx!==7 && dIdx!==10 && dIdx!==11)?'Split':'Stand';
else if(v===7) action = (dIdx<=7)?'Split':'Hit';
else if(v===6) action = (dIdx<=6)?'Split':'Hit';
else if(v===4) action = (dIdx===5||dIdx===6)?'Split':'Hit';
else if(v===3||v===2) action = (dIdx<=7)?'Split':'Hit';
else action = 'Hit';
}
else if(p.isSoft) {
if(p.total>=19) action = 'Stand';
else if(p.total===18) {
if (p.cards.length > 2) action = (dIdx <= 8) ? 'Stand' : 'Hit';
else action = (dIdx>=3 && dIdx<=6)?'Double':(dIdx<=8?'Stand':'Hit');
}
else {
if (p.cards.length > 2) action = 'Hit';
else action = (dIdx>=3 && dIdx<=6)?'Double':'Hit';
}
}
else {
if(p.total>=17) action = 'Stand';
else if(p.total>=13) action = (dIdx<=6)?'Stand':'Hit';
else if(p.total===12) action = (dIdx>=4&&dIdx<=6)?'Stand':'Hit';
else if(p.total===11) action = 'Double';
else if(p.total===10) action = (dIdx<10)?'Double':'Hit';
else if(p.total===9) action = (dIdx>=3&&dIdx<=6)?'Double':'Hit';
else action = 'Hit';
}
if (action === 'Double' && p.cards.length > 2) action = 'Hit';
return action;
}
// --- UPDATED CARD PARSER (Fixes sum errors & extracts string) ---
function getHandInfo(selector) {
const container = document.querySelector(selector);
if (!container) return { cards: [], cardTexts: [], total: 0, isSoft: false, isPair: false };
// Select all card divs that contain 'card-' but are NOT the back
const cardElements = Array.from(container.querySelectorAll('div[class*="card-"]'));
const values = [];
const texts = [];
cardElements.forEach(el => {
const match = el.className.match(/card-\w+-(\w+)/);
if (match && !el.classList.contains('card-back')) {
const rank = match[1].toUpperCase();
texts.push(rank);
let val = 0;
if (['J', 'Q', 'K', '10'].includes(rank)) val = 10;
else if (rank === 'A') val = 11;
else val = parseInt(rank, 10);
if(!isNaN(val)) values.push(val);
}
});
let sum = values.reduce((a, b) => a + b, 0);
let aces = values.filter(v => v === 11).length;
while (sum > 21 && aces > 0) {
sum -= 10;
aces--;
}
return {
cards: values,
cardTexts: texts,
total: sum,
isSoft: aces > 0,
isPair: values.length === 2 && values[0] === values[1]
};
}
function initialize() {
createTrackerPanel(); refreshTrackerUI();
setInterval(() => { if(apiKey) importApiData(true); }, API_SYNC_INTERVAL_MS);
new MutationObserver(() => { if(helperUpdateTimeout) clearTimeout(helperUpdateTimeout); helperUpdateTimeout=setTimeout(updateHelper,200); })
.observe(document.body, {childList:true, subtree:true, attributes:true});
}
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', initialize);
else initialize();
})();