based on S17 basic strategy , all in one tool buttons on the display to skip animations and fast bet buttons
// ==UserScript==
// @name Blackjack ToolKit V3
// @version 3.2
// @description based on S17 basic strategy , all in one tool buttons on the display to skip animations and fast bet buttons
// @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: THE TRACKER ============================
// ============================================================================
// --- Constants & Storage Keys ---
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';
// --- Log IDs ---
const LOG_ID_WIN = 8355; // Blackjack Win (contains Winnings)
const LOG_ID_LOSE = 8354; // Blackjack Loss (contains Losses)
const LOG_ID_PUSH = 8358; // Blackjack Push
const RESULT_LOG_IDS = [LOG_ID_WIN, LOG_ID_LOSE, LOG_ID_PUSH];
const API_SYNC_INTERVAL_MS = 15 * 1000;
const API_PULL_LIMIT = 100; // API default limit
// --- State Variables ---
let apiKey = '';
let results = [];
let totalProfit = 0;
let isTrackerDragging = false;
let currentOpacity = 0.8;
let maxDisplayMatches = 50000;
let isSessionActive = false;
let sessionProfit = 0;
let dailyProfit = 0;
let sessionStartDate = 0;
let isSyncing = false;
let showSettings = false;
let showStatsPanel = false;
let currentStatsTimeframe = 7;
let lastScannedTime = 0;
let isHandActive = false;
let chartInstance = null; // For the graph
let helperUpdateTimeout = null; // <<< ADD THIS LINE for throttling
let isMinimized = false;
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
let shiftKeyHeld = false; // New state variable for Shift key
// --- Initialization ---
function initializeTrackerState() {
apiKey = localStorage.getItem(API_KEY_STORAGE) || '';
results = JSON.parse(localStorage.getItem(STORAGE) || '[]');
totalProfit = parseFloat(localStorage.getItem(PROFIT_STORAGE) || '0');
maxDisplayMatches = parseInt(localStorage.getItem('bj_max_display') || '50000', 10);
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);
if (results.length > 0) {
// FIX: Use setTimeout to ensure this expensive sort runs AFTER the browser finishes rendering the initial page content.
setTimeout(() => {
results.sort((a,b) => b.timestamp - a.timestamp);
refreshTrackerUI();
}, 0);
}
}
initializeTrackerState();
// --- Utility Functions ---
function getTornDayStartTimestamp() {
// Torn server time is UTC. Find the timestamp for the beginning of the current UTC day (00:00:00 UTC).
const now = new Date();
// Use UTC date methods to get the start of the current UTC day.
const utcStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0));
// Convert to Unix timestamp (seconds)
return Math.floor(utcStart.getTime() / 1000);
}
function formatNumberToKMB(num) {
if (typeof num !== 'number' || !isFinite(num)) return 'NaN/Error';
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 generateNewApiKey() {
const url = 'https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=BlackJack+TK&user=log';
window.open(url, '_blank');
alert("Please Paste The API Key into the field below. You may need to refresh after saving the key.");
}
function getCurrentBet() {
const visibleBetInput = document.querySelector('input.bet.input-money[type="text"]');
if (visibleBetInput) {
// Remove commas before parsing
return parseInt(visibleBetInput.value.replace(/,/g, ''), 10) || 0;
}
return 0;
}
function setBet(newBetValue) {
const visibleBetInput = document.querySelector('input.bet.input-money[type="text"]');
const finalBetValue = Math.round(newBetValue);
if (visibleBetInput) {
// Torn's input expects a string representation
visibleBetInput.value = String(finalBetValue);
// Trigger events to update Torn's internal bet state
visibleBetInput.dispatchEvent(new Event('change', { bubbles: true }));
visibleBetInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}
// --- UI Construction (Tracker Panel) ---
function createTrackerPanel() {
if (document.getElementById(PANEL_ID)) return;
// Inject Tracker Styles
const styleSheet = document.createElement("style");
styleSheet.innerText = `
/* --- Main Panel Styles --- */
#${PANEL_ID} {
position: fixed; top: 10px; right: 10px; z-index: 1000;
background-color: #1a1a1a; /* Darker background */
border: 2px solid #FFD700; /* Gold border for visibility */
border-radius: 8px;
padding: 8px;
width: 180px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
font-family: monospace;
color: #E0E0E0; /* Light grey text for better contrast */
line-height: 1.4;
}
/* --- Status Colors --- */
.bj-green { color: #4CAF50; font-weight: bold; } /* Standard Win Green */
.bj-red { color: #FF6F69; font-weight: bold; } /* Brighter red */
.bj-grey { color: #AAAAAA; font-weight: normal; } /* Lighter grey for general data */
.bj-gold { color: #FFD700; font-weight: bold; } /* New: Gold for highlights */
/* --- Tracker Buttons (Sync, Settings, Stats) --- */
.bj-trk-btn {
background: #333; /* Darker background */
border: 1px solid #555;
color: #FFD700; /* Gold text */
cursor: pointer;
border-radius: 4px;
padding: 5px;
transition: background 0.2s, border-color 0.2s;
font-size: 11px;
font-weight: bold;
line-height: 1;
text-transform: uppercase;
}
.bj-trk-btn:hover {
background: #444;
border-color: #FFD700;
}
/* --- Strategy Helper Buttons (Hit, Stand, etc.) --- */
.bj-helper-button {
flex-grow: 1;
padding: 6px 4px;
font-size: 10px;
font-weight: 700;
cursor: pointer;
color: #E0E0E0;
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 4px;
transition: background-color 0.15s;
text-transform: uppercase;
}
.bj-helper-button:hover {
background-color: #444;
}
/* --- Helper Advice Display --- */
.bj-trk-advice {
font-size: 20px;
font-weight: 800;
text-align: center;
margin-bottom: 5px;
padding: 5px 0;
border-bottom: 1px solid #444;
text-shadow: 0 0 5px rgba(0,0,0,0.8);
}
.bj-trk-hand-info { /* New class for hand detail */
font-size: 10px;
color: #aaa;
text-align: center;
margin-bottom: 5px;
}
/* --- Action Highlight Colors --- */
.bj-hit { border-color: #FF5722 !important; background-color: rgba(255, 87, 34, 0.2) !important; }
.bj-stand, .bj-double { border-color: #4CAF50 !important; background-color: rgba(76, 175, 80, 0.2) !important; }
.bj-split { border-color: #2196F3 !important; background-color: rgba(33, 150, 243, 0.2) !important; }
.bj-surrender { border-color: #FFD700 !important; background-color: rgba(255, 215, 0, 0.2) !important; }
.bj-idle { border-color: transparent !important; }
`;
document.head.appendChild(styleSheet);
const panel = document.createElement('div');
panel.id = PANEL_ID;
Object.assign(panel.style, {
position: 'fixed', top: '50px', left: '20px', width: '350px',
background: `rgba(0,0,0,${currentOpacity})`, color: '#fff',
fontFamily: 'monospace', fontSize: '13px', padding: '10px',
borderRadius: '8px', boxShadow: '0 0 15px rgba(0,0,0,0.8)',
zIndex: '999998', transformOrigin: 'top left',
display: 'flex', flexDirection: 'column', gap: '8px',
border: '2px solid transparent', // Dynamic border for helper mode
});
document.body.appendChild(panel);
// Restore Position
try {
const pos = JSON.parse(localStorage.getItem(PANEL_POS_KEY));
if (pos) { panel.style.top = pos.top; panel.style.left = pos.left; }
} catch(e){}
// Header / Drag Handle
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'move', borderBottom: '1px solid #333', paddingBottom: '5px', marginBottom: '5px' });
header.innerHTML = '<span style="font-weight:bold; color: #FFD700;">♠️ BJ Tracker & Helper</span>';
const minimizeBtn = document.createElement('span');
minimizeBtn.textContent = isMinimized ? '[+]' : '[-]'; minimizeBtn.style.cursor = 'pointer';
minimizeBtn.onclick = () => {
isMinimized = !isMinimized;
minimizeBtn.textContent = isMinimized ? '[+]' : '[-]';
const c = document.getElementById('bj-trk-content');
c.style.display = c.style.display === 'none' ? 'flex' : 'none';
localStorage.setItem('bj_tracker_minimized', c.style.display === 'none' ? 'true' : 'false');
};
header.appendChild(minimizeBtn);
panel.appendChild(header);
// Restore minimized state
if (localStorage.getItem('bj_tracker_minimized') === 'true') {
const contentToMinimize = document.getElementById('bj-trk-content');
if(contentToMinimize) contentToMinimize.style.display = 'none';
}
// 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;
panel.style.left = (e.clientX - offsetX) + 'px';
panel.style.top = (e.clientY - offsetY) + '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);
};
// Content Container
const content = document.createElement('div');
content.id = 'bj-trk-content';
Object.assign(content.style, { display: 'flex', flexDirection: 'column', gap: '8px' });
panel.appendChild(content);
// Stats Display
const statsContainer = document.createElement('div');
statsContainer.style.display = 'block';
statsContainer.style.gap = '5px';
statsContainer.innerHTML = `
<div id="bj-total-profit" title="All Time Profit">Life Time Profit: <span class="bj-grey">loading...</span></div>
<div id="bj-daily-profit" title="Profit since Torn Daily Reset">Daily Profit: <span class="bj-grey">--</span></div>
<div id="bj-session-profit" title="Session Profit">Session Profit: <span class="bj-grey">--</span></div>
<div id="bj-winrate" style="grid-column: 1 / -1; text-align: center; font-size: 11px; color: #aaa;">Wins: 0 | Losses: 0 | Pushes: 0</div>
<div id="bj-sync-status" style="grid-column: 1 / -1; text-align: center; font-size: 10px; color: #666;">Next Sync: --</div>
`;
content.appendChild(statsContainer);
// Buttons Row (Session, Stats, Settings, Sync)
const actionRow = document.createElement('div');
Object.assign(actionRow.style, { display: 'flex', gap: '5px', marginTop: '5px' });
const btnSettings = document.createElement('button'); btnSettings.className = 'bj-trk-btn'; btnSettings.textContent = '⚙️'; btnSettings.style.flex = '1';
btnSettings.onclick = () => { showSettings = !showSettings; showStatsPanel = false; refreshTrackerUI(); };
const btnStats = document.createElement('button'); btnStats.className = 'bj-trk-btn'; btnStats.textContent = '📊 Graph'; btnStats.style.flex = '1';
btnStats.onclick = () => { showStatsPanel = !showStatsPanel; showSettings = false; refreshTrackerUI(); if(showStatsPanel) updateGraph(); };
const btnSession = document.createElement('button'); btnSession.className = 'bj-trk-btn'; btnSession.textContent = isSessionActive ? '⏹ Stop' : '▶ Start'; btnSession.style.flex = '1';
btnSession.onclick = toggleSession;
const btnSync = document.createElement('button'); btnSync.className = 'bj-trk-btn'; btnSync.textContent = '🔄'; btnSync.style.flex = '0.5';
btnSync.onclick = () => importApiData(false);
actionRow.append(btnSession, btnStats, btnSettings, btnSync);
content.appendChild(actionRow);
// Persistent Betting Controls Container (FIX: Always visible)
const bettingControls = document.createElement('div');
bettingControls.id = 'bj-betting-controls';
content.appendChild(bettingControls);
// Dynamic Content Container (Helper or Recent Games List)
const dynamicContent = document.createElement('div');
dynamicContent.id = 'bj-dynamic-content';
content.appendChild(dynamicContent);
// Add the persistent list container structure (only once)
dynamicContent.innerHTML = `
<div id="bj-recent-games" style="max-height: 100px; overflow-y: auto; border: 1px solid #333; padding: 4px; background: rgba(0,0,0,0.3);">
</div>
`;
// Settings Panel (Tracker + Helper Settings)
const settingsPanel = document.createElement('div');
settingsPanel.id = 'bj-settings-panel';
Object.assign(settingsPanel.style, { display: 'none', flexDirection: 'column', gap: '8px', padding: '10px', background: '#111', border: '1px solid #444', marginTop: '5px' });
settingsPanel.innerHTML = `
<h4 style="margin:0; border-bottom:1px solid #333; padding-bottom:3px;">API & Data Settings</h4>
<label style="display:block; margin-bottom: 3px; font-size: 0.9em;">API Key: <input type="password" id="bj-api-input" value="${apiKey}" style="width:95%; background:#222; border:1px solid #555; color:white; padding: 2px; box-sizing: border-box;"></label>
<div style="display:flex; gap: 5px; margin-bottom: 5px;">
<button id="bj-save-api" class="bj-trk-btn" style="flex: 1.5; padding: 3px;">Save Key & Full Sync</button>
<button id="bj-generate-api" class="bj-trk-btn" style="flex: 1; padding: 3px;">Generate Key</button>
</div>
<h4 style="margin:5px 0 3px 0; border-bottom:1px solid #333; padding-bottom:3px;">Helper & Bet Settings</h4>
<div style="display:flex; flex-direction: column; gap: 3px;">
<label style="font-size: 0.9em;">
Start Bet:
<input type="text" id="bj-start-bet-input" value="${localStorage.getItem('bj_start_bet_value') || '20000'}"
style="width:80px; text-align:right; background:#333; color:white; border:1px solid #555; padding: 2px;">
</label>
<label style="font-size: 0.9em;">
Multiplier:
<input type="number" id="bj-multiplier-input" value="${localStorage.getItem('bj_multiplier_value') || '2.2'}" step="0.1"
style="width:60px; background:#333; color:white; border:1px solid #555; padding: 2px;">
</label>
</div>
<button id="bj-reset-data" class="bj-trk-btn" style="color: #E53935; border-color: #E53935; margin-top: 8px; padding: 3px;">Reset All Data</button>
`;
content.appendChild(settingsPanel);
setupSettingsEvents();
setupPersistentControls();
// Stats/Graph Panel
const statsPanel = document.createElement('div');
statsPanel.id = 'bj-stats-panel';
Object.assign(statsPanel.style, { display: 'none', flexDirection: 'column', marginTop: '10px', padding: '5px', background: '#1a1a1a', border: '1px solid #444' });
statsPanel.innerHTML = `
<canvas id="bj-graph-canvas" width="320" height="180"></canvas>
<div style="display:flex; gap:5px; 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>
`;
content.appendChild(statsPanel);
statsPanel.querySelectorAll('button').forEach(btn => {
btn.onclick = () => { currentStatsTimeframe = parseInt(btn.getAttribute('data-days')); updateGraph(); }
});
}
function setupSettingsEvents() {
setTimeout(() => {
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'); // Force full scan next time
lastScannedTime = 0;
importApiData(false); // Force silent=false to give feedback
};
document.getElementById('bj-generate-api').onclick = generateNewApiKey;
// --- FIXED RESET LOGIC ---
document.getElementById('bj-reset-data').onclick = () => {
if(confirm("Reset ALL tracking data? This will clear all games, profit, session data, AND the API key, forcing a fresh start.")) {
// Tracker Data Reset
results = []; totalProfit = 0; sessionProfit = 0;
localStorage.removeItem(STORAGE);
localStorage.removeItem(PROFIT_STORAGE);
localStorage.removeItem(LAST_SCANNED_TIMESTAMP);
localStorage.removeItem(SESSION_ACTIVE_KEY);
localStorage.removeItem(SESSION_PROFIT_KEY);
localStorage.removeItem(SESSION_START_KEY);
lastScannedTime = 0;
// API Key Reset
apiKey = '';
localStorage.removeItem(API_KEY_STORAGE);
const apiInput = document.getElementById('bj-api-input');
if(apiInput) apiInput.value = '';
// Clear graph instance (if exists)
if (chartInstance) chartInstance = null;
// Re-initialize state and refresh UI
refreshTrackerUI();
updateHelper(); // Force dynamic content update to list
}
};
// Helper/Betting Events
document.getElementById('bj-start-bet-input').onchange = (e) => localStorage.setItem('bj_start_bet_value', e.target.value);
document.getElementById('bj-multiplier-input').onchange = (e) => {
localStorage.setItem('bj_multiplier_value', e.target.value);
};
}, 500);
}
// Function to create and setup the persistent betting/play controls
function setupPersistentControls() {
const bettingControls = document.getElementById('bj-betting-controls');
if (!bettingControls) return;
// Betting Actions Row
const betRow = document.createElement('div');
Object.assign(betRow.style, { display: 'flex', gap: '5px', marginTop: '5px' });
const btnStart = document.createElement('button'); btnStart.id = 'bj-start-game'; btnStart.className = 'bj-helper-button'; btnStart.textContent = 'PLAY / Deal'; btnStart.style.backgroundColor = '#F9A825'; btnStart.style.color = '#333';
const btnResetBet = document.createElement('button'); btnResetBet.id = 'bj-reset-bet'; btnResetBet.className = 'bj-helper-button'; btnResetBet.textContent = 'Starting Bet';
const btnMult = document.createElement('button'); btnMult.id = 'bj-multiply-bet'; btnMult.className = 'bj-helper-button';
const btnDiv = document.createElement('button'); btnDiv.id = 'bj-divide-bet'; btnDiv.className = 'bj-helper-button';
const mult = parseFloat(localStorage.getItem('bj_multiplier_value') || '2.5').toFixed(2);
btnMult.textContent = `x ${mult}`;
btnDiv.textContent = `/ ${mult}`;
betRow.append(btnStart, btnResetBet, btnMult, btnDiv);
bettingControls.appendChild(betRow);
// Setup events for the persistent buttons (passing null for non-existent game buttons)
setupHelperButtonEvents(btnStart, btnResetBet, btnMult, btnDiv, null, null, null, null);
}
// --- Dynamic Content Rendering ---
function renderRecentGamesList() {
const listContainer = document.getElementById('bj-recent-games');
if (!listContainer) return; // Must have the container created above
listContainer.style.maxHeight = '100px'; // Increased height for better visibility
listContainer.style.overflowY = 'auto';
listContainer.style.border = '1px solid #333';
listContainer.style.padding = '4px';
listContainer.style.background = 'rgba(0,0,0,0.3)';
const sortedResults = results.slice().sort((a,b) => b.timestamp - a.timestamp);
let html = '';
sortedResults.slice(0, maxDisplayMatches).forEach((r, index) => { // ADDED INDEX
let color = '#888'; let sign = '';
let displayProfit = Math.abs(r.profit);
// *** STEP 3 LOGIC INTEGRATION ***
let outcome = r.result.toUpperCase();
if (r.isNatural) {
outcome = 'NATURAL';
} else if (r.result === 'win' && r.isDouble) {
outcome = 'WIN DOUBLE';
} else if (r.result === 'lose' && r.isDouble) {
outcome = 'LOSE DOUBLE';
}
// ... color logic remains the same
if (r.result === 'win') {
color = '#4CAF50';
sign = '+';
}
else if (r.result === 'lose') {
color = '#E53935';
sign = '-';
}
else if (r.result === 'push') {
color = '#FFC107';
sign = '';
}
// ********************************
html += `<div style="font-size:11px; display:flex; justify-content:space-between; border-bottom:1px solid #222;">
<span style="color:#aaa;">#${results.length - index}</span>
<span style="color:${color}; font-weight:bold;">${outcome}</span>
<span style="color:${color};">${sign}$${formatNumberToKMB(displayProfit)}</span>
</div>`;
});
listContainer.innerHTML = html; // Only update the inner content
// NO RETURN STATEMENT NEEDED
}
// Only creates advice and game buttons
function renderHelperContent(advice, handInfo, actionColor) {
const helperContainer = document.createElement('div');
helperContainer.id = 'bj-helper-content';
Object.assign(helperContainer.style, {
padding: '5px',
background: '#1a1a1a',
border: '1px solid #444',
borderRadius: '4px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
});
// 1. Advice Text
const adviceDiv = document.createElement('div');
adviceDiv.className = 'bj-trk-advice';
adviceDiv.id = 'bj-helper-advice-text';
adviceDiv.textContent = advice;
adviceDiv.style.setProperty('--bj-border-color', actionColor);
helperContainer.appendChild(adviceDiv);
// 2. Hand Info
const infoDiv = document.createElement('div');
infoDiv.id = 'bj-helper-hand-total';
infoDiv.style.fontSize = '12px';
infoDiv.style.opacity = '0.9';
infoDiv.textContent = handInfo;
helperContainer.appendChild(infoDiv);
// 3. Game Actions (Dynamic: Hit, Stand, Double, Split)
const gameRow = document.createElement('div');
Object.assign(gameRow.style, { display: 'flex', gap: '5px' });
const btnHit = document.createElement('button'); btnHit.id = 'bj-hit-btn'; btnHit.className = 'bj-helper-button'; btnHit.textContent = 'HIT';
const btnStand = document.createElement('button'); btnStand.id = 'bj-stand-btn'; btnStand.className = 'bj-helper-button'; btnStand.textContent = 'STAND';
const btnDouble = document.createElement('button'); btnDouble.id = 'bj-double-btn'; btnDouble.className = 'bj-helper-button'; btnDouble.textContent = 'DBL';
const btnSplit = document.createElement('button'); btnSplit.id = 'bj-split-btn'; btnSplit.className = 'bj-helper-button'; btnSplit.textContent = 'SPLIT';
gameRow.append(btnHit, btnStand, btnDouble, btnSplit);
helperContainer.appendChild(gameRow);
// Setup dynamic button classes and events
setupHelperButtonClasses(advice, btnHit, btnStand, btnDouble, btnSplit);
// Only set up game button events here (passing null for betting buttons)
setupHelperButtonEvents(null, null, null, null, btnHit, btnStand, btnDouble, btnSplit);
return helperContainer;
}
function setupHelperButtonClasses(advice, btnHit, btnStand, btnDouble, btnSplit) {
// Reset all buttons
[btnHit, btnStand, btnDouble, btnSplit].forEach(btn => btn.className = 'bj-helper-button');
// Highlight the recommended action
if (advice.startsWith(AC.H)) {
btnHit.classList.add('bj-hit');
} else if (advice.startsWith(AC.S)) {
btnStand.classList.add('bj-stand');
} else if (advice.startsWith(AC.D)) {
btnDouble.classList.add('bj-double');
} else if (advice.startsWith(AC.SP)) {
btnSplit.classList.add('bj-split');
}
}
function setupHelperButtonEvents(btnStart, btnResetBet, btnMult, btnDiv, btnHit, btnStand, btnDouble, btnSplit) {
// Game Actions (only set up if buttons exist)
const clickAction = (step, confirm) => {
const area = document.querySelector(`area[data-step="${step}"]`);
if(area) { area.dispatchEvent(new MouseEvent('click', {bubbles:true})); if(confirm) setTimeout(() => { const c=document.querySelector('.action-wrap .confirm-action.yes'); if(c) c.click(); }, 100); }
};
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);
// Betting Actions (only set up if buttons exist - these are the persistent ones)
if(btnStart) btnStart.onclick = () => {
const btn = document.querySelector('a.startGame[data-step="startGame"]');
if(btn) { btn.click(); setTimeout(() => { const yes=document.querySelector('.bet-confirm .yes'); if(yes) yes.click(); }, 100); }
};
if(btnResetBet) btnResetBet.onclick = () => {
const val = document.getElementById('bj-start-bet-input').value;
setBet(parseInt(val.replace(/,/g,''),10));
};
if(btnMult) btnMult.onclick = () => {
const mult = parseFloat(document.getElementById('bj-multiplier-input').value) || 2.5;
setBet(Math.round(getCurrentBet() * mult));
};
if(btnDiv) btnDiv.onclick = () => {
const mult = parseFloat(document.getElementById('bj-multiplier-input').value) || 2.5;
setBet(Math.max(100, Math.round(getCurrentBet() / mult)));
};
}
// This function updates the UI elements with current data (non-freezing)
function refreshTrackerUI() {
const totalEl = document.getElementById('bj-total-profit');
if (totalEl) totalEl.innerHTML = `Life Time Profit: <span class="${totalProfit >= 0 ? 'bj-green' : 'bj-red'}">$${formatNumberToKMB(totalProfit)}</span>`;
const dailyEl = document.getElementById('bj-daily-profit'); // ADD THIS LINE
if (dailyEl) dailyEl.innerHTML = `Daily Profit: <span class="${dailyProfit >= 0 ? 'bj-green' : 'bj-red'}">$${formatNumberToKMB(dailyProfit)}</span>`; // ADD THIS LINE
const sessionEl = document.getElementById('bj-session-profit');
if (sessionEl) sessionEl.innerHTML = `Session Profit: <span class="${isSessionActive ? (sessionProfit >= 0 ? 'bj-green' : 'bj-red') : 'bj-grey'}">${isSessionActive ? '$' + formatNumberToKMB(sessionProfit) : 'Inactive'}</span>`;
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 winrateEl = document.getElementById('bj-winrate');
if (winrateEl) {
winrateEl.innerHTML = `
<span class="bj-green">Wins:</span> ${wins} |
<span class="bj-red">Losses:</span> ${losses} |
<span class="bj-gold">Pushes:</span> ${pushes} |
Total: ${results.length}
`;
// Note: The total matches text 'Total:' is gray by default from the container style,
// but the number will be white (or default)
}
const settingsEl = document.getElementById('bj-settings-panel');
if (settingsEl) settingsEl.style.display = showSettings ? 'flex' : 'none';
const statsEl = document.getElementById('bj-stats-panel');
if (statsEl) statsEl.style.display = showStatsPanel ? 'flex' : 'none';
// Update session button text
const btnSession = document.querySelector('.bj-trk-btn[textContent*="Start"], .bj-trk-btn[textContent*="Stop"]');
if (btnSession) btnSession.textContent = isSessionActive ? '⏹ Stop' : '▶ Start';
// Update sync status with scan status
const status = document.getElementById('bj-sync-status');
if (status) {
const nextSyncTime = new Date(Date.now() + API_SYNC_INTERVAL_MS).toLocaleTimeString();
const scanStatus = lastScannedTime > 1 ? 'Fully Scanned' : (lastScannedTime === 0 ? 'No Data/API Key Missing' : 'Partial Scan (Recent)');
status.textContent = `${isSyncing ? 'Syncing...' : scanStatus} | Next Sync: ${nextSyncTime}`;
}
}
function toggleSession() {
isSessionActive = !isSessionActive;
if (isSessionActive) {
sessionProfit = 0;
sessionStartDate = Date.now();
// Recalculate session profit from existing results
const currentSessionProfit = results
.filter(r => r.timestamp * 1000 >= sessionStartDate)
.reduce((sum, r) => sum + r.profit, 0);
sessionProfit = currentSessionProfit;
} 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();
}
// --- API Sync (Tracking functions) ---
async function importApiData(silent = true) {
if (isSyncing) return;
if (!apiKey) { if(!silent) alert("Please enter your API key in the settings."); return; }
isSyncing = true;
const status = document.getElementById('bj-sync-status');
if(status) status.textContent = "Syncing... (0 games found)";
const initialTotalGames = results.length;
const latestTimestamp = results.length > 0 ? results[0].timestamp : 0;
let oldestTimestamp = results.length > 0 ? results[results.length - 1].timestamp : 0;
let totalNewGames = 0;
let isFullScanNeeded = latestTimestamp === 0 || lastScannedTime < 2 || !silent;
try {
// PHASE 1: FORWARD/CATCHUP SCAN
if (latestTimestamp > 0 || !isFullScanNeeded) {
const fromTime = latestTimestamp > 0 ? latestTimestamp : (Date.now()/1000) - (24 * 3600);
const forwardLogs = await fetchLogs(fromTime);
if (forwardLogs.resultCount > 0) {
processLogs(forwardLogs.resultLogs);}
}
// PHASE 2: BACKWARD/FULL SCAN
if (isFullScanNeeded) {
if(status) status.textContent = `Syncing... Starting full history scan.`;
let currentScanPoint = oldestTimestamp > 0 ? oldestTimestamp : Math.floor(Date.now() / 1000);
let keepScanning = true;
let backwardCount = 0;
let scanIterations = 0;
while (keepScanning && scanIterations < 500) {
if (lastScannedTime > 1 && currentScanPoint < lastScannedTime) {
keepScanning = false;
break;
}
const backwardLogs = await fetchLogs(null, currentScanPoint);
if (backwardLogs.resultCount > 0) {
const oldestLogTimestamp = Math.min(...backwardLogs.resultLogs.map(l => l.timestamp));
processLogs(backwardLogs.resultLogs);
backwardCount = backwardLogs.resultLogs.length;
oldestTimestamp = results.length > 0 ? results[results.length - 1].timestamp : 0;
if(status) status.textContent = `Syncing... Games found: ${results.length} (Batch: ${backwardCount})`;
if (backwardCount < API_PULL_LIMIT || oldestLogTimestamp === currentScanPoint) {
keepScanning = false;
lastScannedTime = 1; // Mark as done only if the API returned less than limit
} else {
currentScanPoint = oldestLogTimestamp - 1;
}
} else {
keepScanning = false;
lastScannedTime = oldestTimestamp === 0 ? currentScanPoint : oldestTimestamp;
}
scanIterations++;
}
localStorage.setItem(LAST_SCANNED_TIMESTAMP, lastScannedTime.toString());
}
totalProfit = results.reduce((sum, r) => sum + r.profit, 0);
sessionProfit = results.filter(r => r.timestamp * 1000 >= sessionStartDate).reduce((sum, r) => sum + r.profit, 0);
totalNewGames = results.length - initialTotalGames;
const tornDayStart = getTornDayStartTimestamp();
dailyProfit = results.filter(r => r.timestamp >= tornDayStart).reduce((sum, r) => sum + r.profit, 0);
localStorage.setItem(STORAGE, JSON.stringify(results));
localStorage.setItem(PROFIT_STORAGE, totalProfit.toString());
localStorage.setItem(SESSION_PROFIT_KEY, sessionProfit.toString());
localStorage.setItem(LAST_SYNC_KEY, Date.now().toString());
if(!silent && totalNewGames > 0) alert(`Synced ${totalNewGames} new games.`);
else if (!silent && totalNewGames === 0) alert("No new games found.");
// FIX: Sorting is now deferred to prevent UI freeze after data update
setTimeout(() => {
results.sort((a,b) => b.timestamp - a.timestamp);
refreshTrackerUI();
if(showStatsPanel) updateGraph();
}, 10);
} catch (e) {
console.error("API Sync Error:", e);
if(!silent) alert(`Sync failed. Check console for details. Error: ${e.message}`);
}
finally {
isSyncing = false;
refreshTrackerUI();
}
}
async function fetchLogs(fromTime = null, toTime = null) {
let resultLogUrl = `https://api.torn.com/user/?selections=log&log=${RESULT_LOG_IDS.join(',')}&key=${apiKey}`;
if (fromTime) {
resultLogUrl += `&from=${fromTime + 1}`;
}
if (toTime) {
resultLogUrl += `&to=${toTime}`;
}
let rawResultLogs = [];
const fetchResultLogs = new Promise(resolve => {
GM.xmlHttpRequest({
method: "GET", url: resultLogUrl,
onload: (r) => {
try { rawResultLogs = Object.values(JSON.parse(r.responseText).log || {}); } catch(e) { console.error("Error fetching result logs:", e); }
resolve();
},
onerror: () => resolve()
});
});
await Promise.all([fetchResultLogs]);
return {
resultLogs: rawResultLogs,
resultCount: rawResultLogs.length
};
}
function processLogs(rawResultLogs) {
const existingResults = new Set(results.map(r => r.timestamp));
// 2. Process Result Logs and Match Bets
for (const log of rawResultLogs) {
if (existingResults.has(log.timestamp)) continue;
let res, prof = 0, bet = 0;
// --- PROFIT CALCULATION FIX ---
// 1. Natural Win Detection (Use the reliable win_state string)
const isNaturalWin = log.log === LOG_ID_WIN && (log.data.win_state || '').includes('natural');
// --- PROFIT CALCULATION FIX: Relying on API Winnings/Losses for Profit ---
if (log.log === LOG_ID_WIN) {
let winningsValue = log.data.winnings || 0;
// The WINNINGS field is the Total Return (Stake + Profit).
// To get the Profit, we calculate the original stake and subtract it from the winnings.
if (isNaturalWin) {
// Natural pays 1.5x profit (2.5x total return). Profit is Winnings / 2.5 * 1.5
const stake = winningsValue / 2.5;
prof = winningsValue - stake; // OR prof = stake * 1.5;
} else {
// Standard/Double Win pays 1x profit (2x total return). Profit is Winnings / 2
const stake = winningsValue / 2;
prof = winningsValue - stake; // OR prof = stake * 1;
}
res = 'win';
} else if (log.log === LOG_ID_LOSE) {
// LOSE: The 'losses' field contains the full stake lost.
prof = -1 * (log.data.losses || 0);
res = 'lose';
} else if (log.log === LOG_ID_PUSH) {
res = 'push';
prof = 0;
}
// -----------------------------------------
if (res) {
results.push({
result: res,
profit: prof,
timestamp: log.timestamp,
logId: log.log,
isNatural: isNaturalWin
});
existingResults.add(log.timestamp);
}
}
}
// --- NEW FUNCTION: Countdown Logic ---
let syncCountdownInterval = null;
function updateSyncCountdown() {
const nextSyncTime = parseInt(localStorage.getItem(LAST_SYNC_KEY) || '0', 10) + API_SYNC_INTERVAL_MS;
const now = Date.now();
const timeLeftMs = nextSyncTime - now;
const syncStatusEl = document.getElementById('bj-sync-status');
if (!syncStatusEl) {
clearInterval(syncCountdownInterval);
syncCountdownInterval = null;
return;
}
if (timeLeftMs > 0) {
const seconds = Math.floor(timeLeftMs / 1000) % 60;
const minutes = Math.floor(timeLeftMs / 1000 / 60);
const minutesStr = String(minutes).padStart(2, '0');
const secondsStr = String(seconds).padStart(2, '0');
syncStatusEl.innerHTML = `Next Sync: ${minutesStr}:${secondsStr}`;
} else {
syncStatusEl.textContent = 'Next Sync: Soon...';
}
}
// --- Dragging Functions ---
function startDragging(e) {
const panel = document.getElementById(PANEL_ID);
if (!panel || e.button !== 0 || !shiftKeyHeld) return; // Only drag on left-click AND Shift hold
isDragging = true;
panel.style.cursor = 'grabbing';
// Calculate the offset from the mouse to the panel's top-left corner
dragOffsetX = e.clientX - panel.getBoundingClientRect().left;
dragOffsetY = e.clientY - panel.getBoundingClientRect().top;
panel.style.position = 'fixed';
e.preventDefault();
}
function stopDragging() {
if (!isDragging) return;
const panel = document.getElementById(PANEL_ID);
if (panel) panel.style.cursor = 'grab';
isDragging = false;
}
function dragPanel(e) {
if (!isDragging) return;
const panel = document.getElementById(PANEL_ID);
if (!panel) return;
// Calculate new position
let newX = e.clientX - dragOffsetX;
let newY = e.clientY - dragOffsetY;
// --- JUMP TO STEP 3: APPLY BOUNDARY CHECKS HERE ---
const panelRect = panel.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// Boundary Check (Cannot go over edges)
newX = Math.max(0, Math.min(newX, windowWidth - panelRect.width));
newY = Math.max(0, Math.min(newY, windowHeight - panelRect.height));
// --- END BOUNDARY CHECK ---
panel.style.left = `${newX}px`;
panel.style.top = `${newY}px`;
}
function updateGraph() {
const canvas = document.getElementById('bj-graph-canvas');
if (!canvas) return;
if (!showStatsPanel) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,canvas.width,canvas.height);
const cutoff = (Date.now()/1000) - (currentStatsTimeframe*24*3600);
const points = results.filter(r => r.timestamp >= cutoff).sort((a,b) => a.timestamp - b.timestamp);
if(points.length > 0) {
let acc = 0;
// Calculate accumulated profit for the selected timeframe
points.forEach(p => { acc += p.profit; });
dailyProfit = acc; // Store the result
} else {
dailyProfit = 0;
}
if(points.length < 2) {
ctx.fillStyle = '#666';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('Not enough data to graph.', canvas.width / 2, canvas.height / 2);
return;
}
let acc = 0;
const data = points.map(p => { acc += p.profit; return { t: p.timestamp, v: acc }; });
const pad = 20;
const W = canvas.width - pad*2; const H = canvas.height - pad*2;
const minV = Math.min(0, ...data.map(d=>d.v)); const maxV = Math.max(0, ...data.map(d=>d.v));
const minT = data[0].t; const maxT = data[data.length-1].t;
const vRange = maxV - minV || 1;
const tRange = maxT - minT || 1;
const mapX = t => pad + ((t-minT)/tRange)*W;
const mapY = v => (canvas.height-pad) - ((v-minV)/vRange)*H;
// Zero Line
const zy = mapY(0);
ctx.strokeStyle = '#444'; ctx.beginPath(); ctx.moveTo(pad, zy); ctx.lineTo(canvas.width-pad, zy); ctx.stroke();
// Graph Line
ctx.strokeStyle = '#FFD700'; ctx.lineWidth = 2; ctx.beginPath();
ctx.moveTo(mapX(data[0].t), mapY(data[0].v));
data.forEach(d => ctx.lineTo(mapX(d.t), mapY(d.v)));
ctx.stroke();
}
// ============================================================================
// ======================== PART 2: THE STRATEGY HELPER =======================
// ============================================================================
const AC = { H: 'Hit', S: 'Stand', D: 'Double', SP: 'Split', SURR: 'Surrender' };
const strategy = {
hard: {
3: Array(11).fill(AC.H), 4: Array(11).fill(AC.H), 5: Array(11).fill(AC.H), 6: Array(11).fill(AC.H), 7: Array(11).fill(AC.H), 8: Array(11).fill(AC.H),
9: [AC.H, AC.D, AC.D, AC.D, AC.D, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
10: [AC.H, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.H, AC.H, AC.H],
11: [AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.H],
12: [AC.H, AC.H, AC.S, AC.S, AC.S, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
13: [AC.S, AC.S, AC.S, AC.S, AC.S, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
14: [AC.S, AC.S, AC.S, AC.S, AC.S, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
15: [AC.S, AC.S, AC.S, AC.S, AC.S, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
16: [AC.S, AC.S, AC.S, AC.S, AC.S, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
17: Array(11).fill(AC.S), 18: Array(11).fill(AC.S), 19: Array(11).fill(AC.S), 20: Array(11).fill(AC.S), 21: Array(11).fill(AC.S),
},
soft: {
13: [AC.H, AC.H, AC.H, AC.D, AC.D, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
14: [AC.H, AC.H, AC.H, AC.D, AC.D, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
15: [AC.H, AC.H, AC.D, AC.D, AC.D, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
16: [AC.H, AC.H, AC.D, AC.D, AC.D, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
17: [AC.H, AC.D, AC.D, AC.D, AC.D, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
18: [AC.S, AC.D, AC.D, AC.D, AC.D, AC.S, AC.S, AC.H, AC.H, AC.H, AC.H],
19: Array(11).fill(AC.S), 20: Array(11).fill(AC.S),
},
pair: {
2: [AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.H, AC.H, AC.H, AC.H, AC.H],
3: [AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.H, AC.H, AC.H, AC.H, AC.H],
4: [AC.H, AC.H, AC.H, AC.SP, AC.SP, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
5: [AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.D, AC.H, AC.H, AC.H],
6: [AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.H, AC.H, AC.H, AC.H, AC.H, AC.H],
7: [AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.H, AC.H, AC.H, AC.H, AC.H],
8: Array(11).fill(AC.SP),
9: [AC.SP, AC.SP, AC.SP, AC.SP, AC.SP, AC.S, AC.SP, AC.SP, AC.S, AC.S, AC.S],
10: Array(11).fill(AC.S),
11: Array(11).fill(AC.SP),
}
};
// --- Helper Logic (Card Reading & Strategy) ---
function getCardValue(el) {
if(!el) return 0;
const match = el.className.match(/card-\w+-(\w+)/);
if(!match) return 0;
const r = match[1];
return (['10','J','Q','K'].includes(r)) ? 10 : (r === 'A' ? 11 : parseInt(r,10));
}
function getHandInfo(sel) {
const els = document.querySelectorAll(`${sel} div[class*="card-"]:not(.card-back)`);
const cards = Array.from(els).map(e => getCardValue(e)).filter(v=>v>0);
let initialSum = cards.reduce((a,b)=>a+b,0);
let aces = cards.filter(v=>v===11).length;
let sum = initialSum;
while(sum>21 && aces-->0) sum-=10;
const isPair = cards.length===2 && cards[0]===cards[1];
// Correct Soft Logic: The sum of the cards (with Aces=11) is greater than the final sum (with reduced Aces),
// AND the final sum is 21 or less. This means at least one Ace is counting as 11.
const isSoft = sum <= 21 && (initialSum > sum); // Use initialSum variable
return { cards, total: sum, isSoft, isPair };
}
// Throttled wrapper for updateHelper to prevent freezing on DOM changes
function throttleUpdateHelper() {
if (helperUpdateTimeout) {
clearTimeout(helperUpdateTimeout);
}
helperUpdateTimeout = setTimeout(() => {
helperUpdateTimeout = null;
updateHelper();
}, 50); // 50ms delay is usually sufficient
}
function updateHelper() {
const dealerEl = document.querySelector('.dealer-cards div[class*="card-"]:not(.card-back)');
const pHand = getHandInfo('.player-cards');
const dHand = getHandInfo('.dealer-cards');
const dynamicContent = document.getElementById('bj-dynamic-content');
const panel = document.getElementById(PANEL_ID);
if(!dynamicContent || showSettings || showStatsPanel) return;
isHandActive = pHand.cards.length >= 2 && pHand.total <= 21 && dHand.cards.length > 0;
let advice = '---';
let handInfo = 'Waiting for hand';
let actionClass = 'bj-idle';
if (isHandActive) {
const dVal = dealerEl ? getCardValue(dealerEl) : 0;
const dIdx = dVal===11 ? 1 : dVal;
if (pHand.total > 21) {
advice = 'Bust (Lose)';
} else {
if(pHand.isPair) {
const pVal = pHand.cards[0]===11?11:pHand.cards[0];
advice = strategy.pair[pVal]?.[dIdx] || AC.H;
} else if(pHand.isSoft) {
advice = strategy.soft[Math.max(13, pHand.total)]?.[dIdx] || AC.S;
} else {
advice = strategy.hard[Math.max(4, pHand.total)]?.[dIdx] || AC.S;
}
}
let handType = '';
if (pHand.isPair) handType = ' (Pair)';
else if (pHand.isSoft) handType = ' (Soft)';
const dCardList = dHand.cards.length >= 2 ? dHand.cards.join(', ') : (dealerEl ? dVal : '?');
const dInfo = dHand.cards.length >= 2 ? `D: ${dHand.total} (${dCardList})` : `D Up: ${dCardList}`;
handInfo = `P: ${pHand.total}${handType} (${pHand.cards.join(', ')}) | ${dInfo}`;
actionClass = `bj-${advice.toLowerCase().split(' ')[0]}`;
// Render Helper UI (Advice and Game Actions)
dynamicContent.innerHTML = '';
dynamicContent.appendChild(renderHelperContent(advice, handInfo, actionClass));
// Set panel border style based on advice
const colorMap = { 'bj-hit': '#FF5722', 'bj-stand': '#4CAF50', 'bj-double': '#4CAF50', 'bj-split': '#2196F3', 'bj-idle': 'transparent' };
panel.style.border = `2px solid ${colorMap[actionClass] || '#FFD700'}`;
} else {
renderRecentGamesList();
panel.style.border = '2px solid transparent';
}
}
// --- Main Execution ---
function initialize() {
createTrackerPanel();
refreshTrackerUI();
// START NEW COUNTDOWN TIMER
updateSyncCountdown();
if (syncCountdownInterval === null) {
syncCountdownInterval = setInterval(updateSyncCountdown, 1000);
}
// END NEW COUNTDOWN TIMER
// Auto-Sync Interval
setInterval(() => { if(apiKey) importApiData(true); }, API_SYNC_INTERVAL_MS);
// Observer for Game State
const observer = new MutationObserver(throttleUpdateHelper);
const gameWrap = document.querySelector('.blackjack-wrap') || document.body;
observer.observe(gameWrap, { childList: true, subtree: true, attributes: true });
// --- NEW: Handle Shift Key State ---
document.addEventListener('keydown', (e) => {
if (e.key === 'Shift') shiftKeyHeld = true;
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Shift') shiftKeyHeld = false;
if (isDragging) stopDragging(); // Stop dragging immediately if Shift is released
});
// --- END Shift Key State ---
const panel = document.getElementById(PANEL_ID);
if (panel) {
// Start dragging event listener (on panel content, not just the header)
panel.addEventListener('mousedown', startDragging);
}
// Global move and stop listeners
document.addEventListener('mousemove', dragPanel);
document.addEventListener('mouseup', stopDragging);
}
// Initial update and sync (silent, partial/catch-up scan)
updateHelper();
importApiData(true);
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();