Set your bank/high score, manage save files, and view a strategy chart on 247blackjack.com
// ==UserScript==
// @name 247 Blackjack Score Setter :)
// @namespace http://tampermonkey.net/
// @version 3.1
// @description Set your bank/high score, manage save files, and view a strategy chart on 247blackjack.com
// @author Damian Berti
// @match https://www.247blackjack.com/
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ── Constants ─────────────────────────────────────────────────────────────
const BANK_KEY = 'com.games247.blackjack.247.Bank';
const HS_KEY = 'com.games247.blackjack.247.Records';
const SAVES_KEY = 'bj_save_files';
// ═══════════════════════════════════════════════════════════════
// STRATEGY CHART DATA
// Actions: H=Hit S=Stand D=Double P=Split R=Surrender(else Hit)
// Dealer columns: 2 3 4 5 6 7 8 9 10 A
// ═══════════════════════════════════════════════════════════════
const DEALER_COLS = ['2','3','4','5','6','7','8','9','10','A'];
const HARD = [
{ hand:'8', row:['H','H','H','H','H','H','H','H','H','H'] },
{ hand:'9', row:['H','D','D','D','D','H','H','H','H','H'] },
{ hand:'10', row:['D','D','D','D','D','D','D','D','H','H'] },
{ hand:'11', row:['D','D','D','D','D','D','D','D','D','H'] },
{ hand:'12', row:['H','H','S','S','S','H','H','H','H','H'] },
{ hand:'13', row:['S','S','S','S','S','H','H','H','H','H'] },
{ hand:'14', row:['S','S','S','S','S','H','H','H','H','H'] },
{ hand:'15', row:['S','S','S','S','S','H','H','H','R','H'] },
{ hand:'16', row:['S','S','S','S','S','H','H','R','R','R'] },
{ hand:'17', row:['S','S','S','S','S','S','S','S','S','S'] },
{ hand:'18+', row:['S','S','S','S','S','S','S','S','S','S'] },
];
const SOFT = [
{ hand:'A+2', row:['H','H','H','D','D','H','H','H','H','H'] },
{ hand:'A+3', row:['H','H','H','D','D','H','H','H','H','H'] },
{ hand:'A+4', row:['H','H','D','D','D','H','H','H','H','H'] },
{ hand:'A+5', row:['H','H','D','D','D','H','H','H','H','H'] },
{ hand:'A+6', row:['H','D','D','D','D','H','H','H','H','H'] },
{ hand:'A+7', row:['S','D','D','D','D','S','S','H','H','H'] },
{ hand:'A+8', row:['S','S','S','S','S','S','S','S','S','S'] },
{ hand:'A+9', row:['S','S','S','S','S','S','S','S','S','S'] },
];
const PAIRS = [
{ hand:'A+A', row:['P','P','P','P','P','P','P','P','P','P'] },
{ hand:'2+2', row:['P','P','P','P','P','P','H','H','H','H'] },
{ hand:'3+3', row:['P','P','P','P','P','P','H','H','H','H'] },
{ hand:'4+4', row:['H','H','H','P','P','H','H','H','H','H'] },
{ hand:'5+5', row:['D','D','D','D','D','D','D','D','H','H'] },
{ hand:'6+6', row:['P','P','P','P','P','H','H','H','H','H'] },
{ hand:'7+7', row:['P','P','P','P','P','P','H','H','H','H'] },
{ hand:'8+8', row:['P','P','P','P','P','P','P','P','P','P'] },
{ hand:'9+9', row:['P','P','P','P','P','S','P','P','S','S'] },
{ hand:'10+10', row:['S','S','S','S','S','S','S','S','S','S'] },
];
const ACTION_META = {
H: { label:'H', title:'Hit', bg:'#1a3a1a', color:'#6fcf97', border:'#2a6a3a' },
S: { label:'S', title:'Stand', bg:'#3a1a1a', color:'#eb8080', border:'#7a3030' },
D: { label:'D', title:'Double Down', bg:'#1a2a3a', color:'#7ab4f8', border:'#2a4a7a' },
P: { label:'P', title:'Split', bg:'#2a2a1a', color:'#f0d060', border:'#6a6020' },
R: { label:'R', title:'Surrender (→ Hit)', bg:'#2a1a2a', color:'#c07af0', border:'#5a2a6a' },
};
// ── Styles ────────────────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
#bj-bank-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999999;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
#bj-panels-row {
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 8px;
margin-bottom: 8px;
}
/* ── Toggle Button ── */
#bj-bank-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
background: linear-gradient(135deg, #1a472a, #2d6a4f);
color: #f0d060;
border: 2px solid #f0d060;
border-radius: 50px;
padding: 7px 15px;
cursor: pointer;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.5px;
box-shadow: 0 4px 14px rgba(0,0,0,0.55);
transition: background 0.2s, transform 0.1s;
user-select: none;
}
#bj-bank-toggle:hover { background: linear-gradient(135deg, #22573a, #3a8a63); transform: scale(1.04); }
#bj-bank-toggle .chip-icon { font-size: 15px; line-height: 1; }
/* ── Shared panel boxes ── */
#bj-strat-box,
#bj-save-box,
#bj-bank-box {
display: none;
flex-direction: column;
gap: 8px;
background: linear-gradient(160deg, #1b2a1b, #0f1f0f);
border: 2px solid #f0d060;
border-radius: 12px;
padding: 12px 14px 11px;
box-shadow: 0 8px 28px rgba(0,0,0,0.7);
animation: bj-fadein 0.18s ease;
align-self: flex-end;
}
#bj-save-box,
#bj-bank-box { min-width: 240px; }
#bj-strat-box { min-width: 0; }
@keyframes bj-fadein {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
#bj-strat-box.open,
#bj-save-box.open,
#bj-bank-box.open { display: flex; }
/* ── Shared typography ── */
.bj-panel-title {
color: #f0d060;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.4px;
display: flex;
align-items: center;
gap: 6px;
border-bottom: 1px solid rgba(240,208,96,0.25);
padding-bottom: 6px;
margin-bottom: 0;
}
.bj-section-title {
color: #f0d060;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.3px;
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 1px;
}
.bj-divider { border: none; border-top: 1px solid rgba(240,208,96,0.18); margin: 1px 0; }
/* ── Bank/HS inputs ── */
#bj-bank-label, #bj-hs-label { color: #b5c9b5; font-size: 11px; margin-bottom: 1px; }
#bj-bank-input, #bj-hs-input {
background: #0d1a0d; color: #f0d060; border: 1.5px solid #3a8a63;
border-radius: 7px; padding: 6px 9px; font-size: 13px; font-weight: 600;
width: 100%; box-sizing: border-box; outline: none; transition: border-color 0.15s;
}
#bj-bank-input:focus, #bj-hs-input:focus { border-color: #f0d060; }
#bj-bank-input::placeholder, #bj-hs-input::placeholder { color: #3a5a3a; font-weight: 400; }
#bj-bank-input::-webkit-outer-spin-button, #bj-bank-input::-webkit-inner-spin-button,
#bj-hs-input::-webkit-outer-spin-button, #bj-hs-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#bj-bank-input[type=number], #bj-hs-input[type=number] { -moz-appearance: textfield; }
#bj-bank-set-btn, #bj-hs-set-btn {
background: linear-gradient(135deg, #f0d060, #c8a820); color: #0f1f0f;
border: none; border-radius: 7px; padding: 7px 0; font-size: 12px; font-weight: 700;
cursor: pointer; width: 100%; letter-spacing: 0.5px;
box-shadow: 0 2px 7px rgba(0,0,0,0.3); transition: background 0.15s, transform 0.1s;
}
#bj-bank-set-btn:hover, #bj-hs-set-btn:hover { background: linear-gradient(135deg, #ffe87a, #dab830); transform: scale(1.02); }
#bj-bank-set-btn:active, #bj-hs-set-btn:active { transform: scale(0.98); }
#bj-bank-status, #bj-hs-status { font-size: 11px; text-align: center; min-height: 14px; color: #6fcf97; font-weight: 600; }
#bj-bank-status.error, #bj-hs-status.error { color: #eb5757; }
#bj-bank-current, #bj-hs-current { font-size: 10.5px; color: #7a9a7a; text-align: center; }
#bj-bank-current span, #bj-hs-current span { color: #a8d5b8; font-weight: 600; }
#bj-watermark { color: #3a5a3a; font-size: 9.5px; text-align: center; margin-top: 1px; letter-spacing: 0.3px; }
/* ── Save file manager ── */
#bj-new-save-row { display: flex; gap: 6px; align-items: center; }
#bj-new-save-input {
flex: 1; background: #0d1a0d; color: #f0d060; border: 1.5px solid #3a8a63;
border-radius: 7px; padding: 6px 9px; font-size: 12px; font-weight: 600;
outline: none; transition: border-color 0.15s; min-width: 0;
}
#bj-new-save-input:focus { border-color: #f0d060; }
#bj-new-save-input::placeholder { color: #3a5a3a; font-weight: 400; }
#bj-new-save-btn {
background: linear-gradient(135deg, #f0d060, #c8a820); color: #0f1f0f; border: none;
border-radius: 7px; padding: 6px 11px; font-size: 12px; font-weight: 700;
cursor: pointer; white-space: nowrap; transition: background 0.15s, transform 0.1s;
}
#bj-new-save-btn:hover { background: linear-gradient(135deg, #ffe87a, #dab830); transform: scale(1.03); }
#bj-new-save-btn:active { transform: scale(0.97); }
#bj-save-status { font-size: 11px; text-align: center; min-height: 14px; color: #6fcf97; font-weight: 600; }
#bj-save-status.error { color: #eb5757; }
#bj-save-list { display: flex; flex-direction: column; gap: 6px; max-height: 200px; overflow-y: auto; padding-right: 2px; }
#bj-save-list::-webkit-scrollbar { width: 4px; }
#bj-save-list::-webkit-scrollbar-track { background: #0d1a0d; border-radius: 4px; }
#bj-save-list::-webkit-scrollbar-thumb { background: #3a8a63; border-radius: 4px; }
.bj-save-empty { color: #3a5a3a; font-size: 11px; text-align: center; padding: 10px 0 4px; font-style: italic; }
.bj-save-card {
background: rgba(13,26,13,0.7); border: 1.5px solid #2a5a3a; border-radius: 9px;
padding: 7px 9px; display: flex; flex-direction: column; gap: 4px;
transition: border-color 0.15s, background 0.15s; position: relative; overflow: hidden;
}
.bj-save-card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg, transparent, rgba(240,208,96,0.12), transparent);
}
.bj-save-card:hover { border-color: #3a8a63; background: rgba(13,26,13,0.95); }
.bj-save-card.active-save { border-color: #f0d060 !important; background: rgba(30,46,20,0.9) !important; }
.bj-save-card.active-save::before { background: linear-gradient(90deg, transparent, rgba(240,208,96,0.35), transparent); }
.bj-save-card-top { display: flex; align-items: center; gap: 5px; }
.bj-save-name { flex: 1; color: #f0d060; font-weight: 700; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bj-save-name-input {
flex: 1; background: #0d1a0d; color: #f0d060; border: 1.5px solid #f0d060;
border-radius: 5px; padding: 2px 6px; font-size: 12px; font-weight: 600; outline: none; min-width: 0;
}
.bj-save-actions { display: flex; gap: 3px; flex-shrink: 0; }
.bj-icon-btn {
background: none; border: 1.5px solid; border-radius: 5px; width: 23px; height: 23px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 11px; transition: background 0.15s, transform 0.1s; padding: 0; line-height: 1;
}
.bj-icon-btn:active { transform: scale(0.93); }
.bj-btn-load { border-color: #3a8a63; color: #6fcf97; }
.bj-btn-load:hover { background: rgba(58,138,99,0.25); }
.bj-btn-overwrite { border-color: #5a7aaa; color: #8ab4f8; }
.bj-btn-overwrite:hover { background: rgba(90,122,170,0.25); }
.bj-btn-rename-ok { border-color: #6fcf97; color: #6fcf97; }
.bj-btn-rename-ok:hover { background: rgba(111,207,151,0.2); }
.bj-btn-rename { border-color: #5a7aaa; color: #8ab4f8; }
.bj-btn-rename:hover { background: rgba(90,122,170,0.25); }
.bj-btn-delete { border-color: #7a3030; color: #eb5757; }
.bj-btn-delete:hover { background: rgba(235,87,87,0.2); }
.bj-save-meta { display: flex; gap: 8px; font-size: 10.5px; color: #7a9a7a; }
.bj-save-meta span { display: flex; align-items: center; gap: 2px; }
.bj-save-meta b { color: #a8d5b8; font-weight: 600; }
.bj-active-badge { font-size: 9px; font-weight: 700; color: #0f1f0f; background: #f0d060; border-radius: 3px; padding: 1px 4px; flex-shrink: 0; }
/* ── Shared confirmation row (used for both load and overwrite) ── */
.bj-confirm-row {
display: none; align-items: center; gap: 5px; margin-top: 1px;
}
.bj-confirm-row.visible { display: flex; }
.bj-confirm-row span { flex: 1; font-size: 10.5px; font-weight: 600; }
.bj-confirm-row.load-confirm span { color: #f0a040; }
.bj-confirm-row.overwrite-confirm span { color: #8ab4f8; }
.bj-btn-confirm-yes {
background: linear-gradient(135deg, #eb5757, #b33); color: #fff; border: none;
border-radius: 5px; padding: 3px 8px; font-size: 10.5px; font-weight: 700; cursor: pointer;
}
.bj-btn-confirm-yes:hover { opacity: 0.85; }
/* Green variant for overwrite confirm */
.bj-btn-confirm-yes.green {
background: linear-gradient(135deg, #2d6a4f, #1a472a);
}
.bj-btn-confirm-yes.green:hover { opacity: 0.85; }
.bj-btn-confirm-no {
background: none; border: 1px solid #5a5a5a; color: #9a9a9a;
border-radius: 5px; padding: 3px 8px; font-size: 10.5px; font-weight: 700; cursor: pointer;
}
.bj-btn-confirm-no:hover { opacity: 0.75; }
/* ══════════════════════════════════════════
STRATEGY CHART
══════════════════════════════════════════ */
#bj-strat-tabs { display: flex; gap: 4px; }
.bj-strat-tab {
flex: 1; background: rgba(13,26,13,0.6); color: #7a9a7a;
border: 1.5px solid #2a5a3a; border-radius: 6px; padding: 4px 0;
font-size: 11px; font-weight: 700; cursor: pointer; text-align: center;
transition: background 0.15s, color 0.15s, border-color 0.15s; letter-spacing: 0.3px;
}
.bj-strat-tab:hover { color: #b5c9b5; border-color: #3a8a63; }
.bj-strat-tab.active { background: rgba(240,208,96,0.12); color: #f0d060; border-color: #f0d060; }
#bj-strat-table-wrap { overflow-x: auto; }
#bj-strat-table-wrap::-webkit-scrollbar { height: 4px; }
#bj-strat-table-wrap::-webkit-scrollbar-track { background: #0d1a0d; border-radius: 4px; }
#bj-strat-table-wrap::-webkit-scrollbar-thumb { background: #3a8a63; border-radius: 4px; }
.bj-strat-table { border-collapse: collapse; font-size: 10.5px; width: 100%; }
.bj-strat-table th {
background: rgba(240,208,96,0.1); color: #f0d060; font-weight: 700;
padding: 3px 5px; text-align: center; border: 1px solid rgba(240,208,96,0.15);
font-size: 10px; letter-spacing: 0.3px;
}
.bj-strat-table th.hand-col { color: #b5c9b5; background: rgba(13,26,13,0.8); min-width: 38px; }
.bj-strat-table td {
padding: 3px 4px; text-align: center; border: 1px solid rgba(255,255,255,0.04);
font-weight: 700; font-size: 10px; min-width: 20px; letter-spacing: 0.2px;
transition: filter 0.1s;
}
.bj-strat-table td:hover { filter: brightness(1.45); cursor: default; }
.bj-strat-table td.hand-label {
color: #b5c9b5; background: rgba(13,26,13,0.5); font-weight: 600;
font-size: 10px; white-space: nowrap; text-align: right; padding-right: 7px;
}
#bj-strat-legend { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 2px; }
.bj-legend-item { display: flex; align-items: center; gap: 3px; font-size: 9.5px; color: #7a9a7a; }
.bj-legend-swatch {
width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(255,255,255,0.12);
font-size: 9px; font-weight: 700; display: flex; align-items: center; justify-content: center;
}
`;
document.head.appendChild(style);
// ── HTML ──────────────────────────────────────────────────────────────────
const panel = document.createElement('div');
panel.id = 'bj-bank-panel';
panel.innerHTML = `
<div id="bj-panels-row">
<!-- ── STRATEGY CHART ── -->
<div id="bj-strat-box">
<div class="bj-panel-title">♠ Strategy Chart</div>
<div id="bj-strat-tabs">
<button class="bj-strat-tab active" data-tab="hard">Hard</button>
<button class="bj-strat-tab" data-tab="soft">Soft</button>
<button class="bj-strat-tab" data-tab="pairs">Pairs</button>
</div>
<div id="bj-strat-table-wrap"></div>
<div id="bj-strat-legend"></div>
</div>
<!-- ── SAVE FILE PANEL ── -->
<div id="bj-save-box">
<div class="bj-panel-title"><span>💾</span> Save Files</div>
<div class="bj-section-title">➕ Create New Save</div>
<div id="bj-new-save-row">
<input id="bj-new-save-input" type="text" maxlength="28" placeholder="Save name…" />
<button id="bj-new-save-btn">Save</button>
</div>
<div id="bj-save-status"></div>
<hr class="bj-divider" />
<div class="bj-section-title">📂 Saved Games</div>
<div id="bj-save-list">
<div class="bj-save-empty">No saves yet — create one above!</div>
</div>
</div>
<!-- ── BANK / HS EDITOR ── -->
<div id="bj-bank-box">
<div class="bj-panel-title"><span class="chip-icon">🪙</span> Editor</div>
<div class="bj-section-title">💰 Bank</div>
<div id="bj-bank-label">Enter new bank amount</div>
<input id="bj-bank-input" type="number" min="0" placeholder="e.g. 50000" />
<button id="bj-bank-set-btn">Set Bank & Reload</button>
<div id="bj-bank-status"></div>
<div id="bj-bank-current">Current: <span id="bj-bank-cur-val">—</span></div>
<hr class="bj-divider" />
<div class="bj-section-title">🏆 High Score</div>
<div id="bj-hs-label">Enter new high score</div>
<input id="bj-hs-input" type="number" min="0" placeholder="e.g. 999999" />
<button id="bj-hs-set-btn">Set High Score & Reload</button>
<div id="bj-hs-status"></div>
<div id="bj-hs-current">Current: <span id="bj-hs-cur-val">—</span></div>
<div id="bj-watermark">Made by Damian B</div>
</div>
</div>
<button id="bj-bank-toggle"><span class="chip-icon">🃏</span> BJ Hack Client</button>
`;
document.body.appendChild(panel);
// ── Element refs ──────────────────────────────────────────────────────────
const toggleBtn = document.getElementById('bj-bank-toggle');
const stratBox = document.getElementById('bj-strat-box');
const saveBox = document.getElementById('bj-save-box');
const bankBox = document.getElementById('bj-bank-box');
const bankInput = document.getElementById('bj-bank-input');
const bankBtn = document.getElementById('bj-bank-set-btn');
const bankStatus = document.getElementById('bj-bank-status');
const bankCurVal = document.getElementById('bj-bank-cur-val');
const hsInput = document.getElementById('bj-hs-input');
const hsBtn = document.getElementById('bj-hs-set-btn');
const hsStatus = document.getElementById('bj-hs-status');
const hsCurVal = document.getElementById('bj-hs-cur-val');
const newSaveInput = document.getElementById('bj-new-save-input');
const newSaveBtn = document.getElementById('bj-new-save-btn');
const saveStatus = document.getElementById('bj-save-status');
const saveList = document.getElementById('bj-save-list');
const stratTableWrap = document.getElementById('bj-strat-table-wrap');
const stratLegend = document.getElementById('bj-strat-legend');
const stratTabs = document.querySelectorAll('.bj-strat-tab');
// ═══════════════════════════════════════════════════════════════
// STRATEGY CHART
// ═══════════════════════════════════════════════════════════════
let activeStratTab = 'hard';
function buildStratTable(data) {
const table = document.createElement('table');
table.className = 'bj-strat-table';
// Header
const thead = document.createElement('thead');
const hr = document.createElement('tr');
const cornerTh = document.createElement('th');
cornerTh.className = 'hand-col';
cornerTh.textContent = 'vs →';
hr.appendChild(cornerTh);
DEALER_COLS.forEach(c => {
const th = document.createElement('th');
th.textContent = c;
hr.appendChild(th);
});
thead.appendChild(hr);
table.appendChild(thead);
// Body
const tbody = document.createElement('tbody');
data.forEach(({ hand, row }) => {
const tr = document.createElement('tr');
const handTd = document.createElement('td');
handTd.className = 'hand-label';
handTd.textContent = hand;
tr.appendChild(handTd);
row.forEach(action => {
const td = document.createElement('td');
const meta = ACTION_META[action] || ACTION_META['H'];
td.textContent = meta.label;
td.title = meta.title;
td.style.background = meta.bg;
td.style.color = meta.color;
td.style.borderColor = meta.border + '55';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
}
function buildLegend() {
stratLegend.innerHTML = '';
Object.values(ACTION_META).forEach(meta => {
const item = document.createElement('div');
item.className = 'bj-legend-item';
item.innerHTML = `
<div class="bj-legend-swatch" style="background:${meta.bg};color:${meta.color};border-color:${meta.border}55">${meta.label}</div>
<span>${meta.title}</span>
`;
stratLegend.appendChild(item);
});
}
function renderStratChart(tab) {
stratTableWrap.innerHTML = '';
const dataMap = { hard: HARD, soft: SOFT, pairs: PAIRS };
stratTableWrap.appendChild(buildStratTable(dataMap[tab]));
}
stratTabs.forEach(tab => {
tab.addEventListener('click', () => {
stratTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
activeStratTab = tab.dataset.tab;
renderStratChart(activeStratTab);
});
});
// ═══════════════════════════════════════════════════════════════
// SAVE FILE LOGIC
// ═══════════════════════════════════════════════════════════════
let activeSaveId = null;
function loadSaves() {
try { return JSON.parse(localStorage.getItem(SAVES_KEY)) || []; }
catch { return []; }
}
function writeSaves(saves) { localStorage.setItem(SAVES_KEY, JSON.stringify(saves)); }
function captureGameState() {
return {
bank: JSON.parse(localStorage.getItem(BANK_KEY) || 'null'),
highScore: JSON.parse(localStorage.getItem(HS_KEY) || 'null'),
};
}
function applyGameState(state) {
if (state.bank != null) localStorage.setItem(BANK_KEY, JSON.stringify(state.bank));
if (state.highScore != null) localStorage.setItem(HS_KEY, JSON.stringify(state.highScore));
}
function getBankVal(state) { return state?.bank?.bank != null ? state.bank.bank : null; }
function getHSVal(state) { return state?.highScore?.highScore != null ? state.highScore.highScore : null; }
function fmtNum(n) { return n != null ? Number(n).toLocaleString() : '?'; }
function timestamp() { return new Date().toLocaleString(undefined, { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' }); }
function escHtml(str) { return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
function setStatus(el, msg, isError = false, duration = 0) {
el.textContent = msg;
el.className = isError ? 'error' : '';
if (duration > 0) setTimeout(() => { el.textContent = ''; el.className = ''; }, duration);
}
function renderSaves() {
const saves = loadSaves();
saveList.innerHTML = '';
if (saves.length === 0) {
saveList.innerHTML = '<div class="bj-save-empty">No saves yet — create one above!</div>';
return;
}
saves.forEach((save) => {
const isActive = save.id === activeSaveId;
const card = document.createElement('div');
card.className = 'bj-save-card' + (isActive ? ' active-save' : '');
card.dataset.id = save.id;
const bankVal = fmtNum(getBankVal(save.state));
const hsVal = fmtNum(getHSVal(save.state));
card.innerHTML = `
<div class="bj-save-card-top">
<span class="bj-save-name" title="${escHtml(save.name)}">${escHtml(save.name)}</span>
${isActive ? '<span class="bj-active-badge">ACTIVE</span>' : ''}
<div class="bj-save-actions">
<button class="bj-icon-btn bj-btn-load" title="Load save">▶</button>
<button class="bj-icon-btn bj-btn-overwrite" title="Overwrite save with current game">💾</button>
<button class="bj-icon-btn bj-btn-rename" title="Rename save">✏️</button>
<button class="bj-icon-btn bj-btn-delete" title="Delete save">🗑</button>
</div>
</div>
<div class="bj-save-meta">
<span>💰 <b>${bankVal}</b></span>
<span>🏆 <b>${hsVal}</b></span>
<span style="margin-left:auto;font-size:9.5px;color:#4a6a4a">${escHtml(save.date)}</span>
</div>
<div class="bj-confirm-row load-confirm">
<span>⚠ Load & reload?</span>
<button class="bj-btn-confirm-yes">Yes</button>
<button class="bj-btn-confirm-no">No</button>
</div>
<div class="bj-confirm-row overwrite-confirm">
<span>💾 Overwrite with current?</span>
<button class="bj-btn-confirm-yes green">Yes</button>
<button class="bj-btn-confirm-no">No</button>
</div>
`;
// ── Load ──
const loadBtn = card.querySelector('.bj-btn-load');
const loadConfirmRow = card.querySelector('.bj-confirm-row.load-confirm');
const loadYes = loadConfirmRow.querySelector('.bj-btn-confirm-yes');
const loadNo = loadConfirmRow.querySelector('.bj-btn-confirm-no');
loadBtn.addEventListener('click', () => {
overwriteConfirmRow.classList.remove('visible'); // close overwrite if open
loadConfirmRow.classList.toggle('visible');
});
loadNo.addEventListener('click', () => loadConfirmRow.classList.remove('visible'));
loadYes.addEventListener('click', () => {
activeSaveId = save.id;
applyGameState(save.state);
setStatus(saveStatus, `✓ "${save.name}" loaded! Reloading…`);
setTimeout(() => location.reload(), 700);
});
// ── Overwrite ──
const overwriteBtn = card.querySelector('.bj-btn-overwrite');
const overwriteConfirmRow = card.querySelector('.bj-confirm-row.overwrite-confirm');
const overwriteYes = overwriteConfirmRow.querySelector('.bj-btn-confirm-yes');
const overwriteNo = overwriteConfirmRow.querySelector('.bj-btn-confirm-no');
overwriteBtn.addEventListener('click', () => {
loadConfirmRow.classList.remove('visible'); // close load if open
overwriteConfirmRow.classList.toggle('visible');
});
overwriteNo.addEventListener('click', () => overwriteConfirmRow.classList.remove('visible'));
overwriteYes.addEventListener('click', () => {
const saves2 = loadSaves();
const idx = saves2.findIndex(s => s.id === save.id);
if (idx === -1) { setStatus(saveStatus, 'Save not found!', true, 2000); return; }
saves2[idx].state = captureGameState();
saves2[idx].date = timestamp();
writeSaves(saves2);
activeSaveId = save.id;
setStatus(saveStatus, `✓ "${saves2[idx].name}" overwritten!`, false, 2000);
renderSaves();
});
// ── Rename ──
const renameBtn = card.querySelector('.bj-btn-rename');
const nameDisplay = card.querySelector('.bj-save-name');
renameBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'text'; input.maxLength = 28; input.className = 'bj-save-name-input'; input.value = save.name;
nameDisplay.replaceWith(input);
renameBtn.className = 'bj-icon-btn bj-btn-rename-ok'; renameBtn.title = 'Confirm rename'; renameBtn.textContent = '✓';
input.focus(); input.select();
function commitRename() {
const newName = input.value.trim();
if (!newName) { setStatus(saveStatus, 'Name cannot be empty.', true, 2000); return; }
const saves2 = loadSaves(); const idx = saves2.findIndex(s => s.id === save.id);
if (idx !== -1) { saves2[idx].name = newName; writeSaves(saves2); save.name = newName; }
setStatus(saveStatus, `✓ Renamed to "${newName}"`, false, 2000);
renderSaves();
}
renameBtn.addEventListener('click', commitRename, { once: true });
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') renderSaves(); });
});
// ── Delete ──
const deleteBtn = card.querySelector('.bj-btn-delete');
let deleteConfirm = false;
deleteBtn.addEventListener('click', () => {
if (!deleteConfirm) {
deleteConfirm = true;
deleteBtn.style.background = 'rgba(235,87,87,0.25)';
deleteBtn.style.borderColor = '#eb5757';
setTimeout(() => { deleteConfirm = false; deleteBtn.style.background = ''; deleteBtn.style.borderColor = ''; }, 1500);
return;
}
const saves2 = loadSaves().filter(s => s.id !== save.id);
writeSaves(saves2);
if (activeSaveId === save.id) activeSaveId = null;
setStatus(saveStatus, `🗑 "${save.name}" deleted.`, false, 2200);
renderSaves();
});
saveList.appendChild(card);
});
}
function createSave() {
const name = newSaveInput.value.trim();
if (!name) { setStatus(saveStatus, 'Enter a save name.', true, 2000); return; }
const saves = loadSaves();
if (saves.some(s => s.name.toLowerCase() === name.toLowerCase())) {
setStatus(saveStatus, 'A save with that name already exists.', true, 2500); return;
}
const newSave = { id: Date.now().toString(36) + Math.random().toString(36).slice(2,6), name, date: timestamp(), state: captureGameState() };
saves.push(newSave); writeSaves(saves);
activeSaveId = newSave.id;
newSaveInput.value = '';
setStatus(saveStatus, `✓ "${name}" created & set as active!`, false, 2500);
renderSaves();
}
newSaveBtn.addEventListener('click', createSave);
newSaveInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') createSave(); });
newSaveInput.addEventListener('input', () => { if (saveStatus.classList.contains('error')) setStatus(saveStatus, ''); });
// ═══════════════════════════════════════════════════════════════
// BANK / HS EDITOR
// ═══════════════════════════════════════════════════════════════
function getStoredValue(key, field) {
try { const raw = localStorage.getItem(key); if (raw) { const p = JSON.parse(raw); return p[field] != null ? p[field] : '?'; } }
catch {} return 'not set';
}
function formatDisplay(val) { const n = Number(val); return Number.isFinite(n) ? n.toLocaleString() : val; }
function refreshCurrentDisplay() {
bankCurVal.textContent = formatDisplay(getStoredValue(BANK_KEY, 'bank'));
hsCurVal.textContent = formatDisplay(getStoredValue(HS_KEY, 'highScore'));
}
function doSet(inputEl, statusEl, key, fieldName, label) {
const raw = inputEl.value.trim();
if (raw === '') { setStatus(statusEl, 'Please enter a number.', true); return; }
const amount = Number(raw);
if (!Number.isFinite(amount) || amount < 0) { setStatus(statusEl, 'Enter a valid positive number.', true); return; }
const finalAmount = Math.floor(amount);
try { localStorage.setItem(key, JSON.stringify({ [fieldName]: finalAmount })); }
catch { setStatus(statusEl, 'Failed to write localStorage.', true); return; }
setStatus(statusEl, `✓ ${label} set to ${finalAmount.toLocaleString()}! Reloading…`);
setTimeout(() => location.reload(), 800);
}
bankBtn.addEventListener('click', () => doSet(bankInput, bankStatus, BANK_KEY, 'bank', 'Bank'));
bankInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSet(bankInput, bankStatus, BANK_KEY, 'bank', 'Bank'); });
bankInput.addEventListener('input', () => { if (bankStatus.classList.contains('error')) setStatus(bankStatus, ''); });
hsBtn.addEventListener('click', () => doSet(hsInput, hsStatus, HS_KEY, 'highScore', 'High Score'));
hsInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSet(hsInput, hsStatus, HS_KEY, 'highScore', 'High Score'); });
hsInput.addEventListener('input', () => { if (hsStatus.classList.contains('error')) setStatus(hsStatus, ''); });
// ── Toggle (all three panels) ─────────────────────────────────────────────
toggleBtn.addEventListener('click', () => {
const isOpen = bankBox.classList.contains('open');
bankBox.classList.toggle('open', !isOpen);
saveBox.classList.toggle('open', !isOpen);
stratBox.classList.toggle('open', !isOpen);
if (!isOpen) {
refreshCurrentDisplay();
renderSaves();
renderStratChart(activeStratTab);
buildLegend();
newSaveInput.focus();
}
});
})();