/* BT Hot Stove GM v0.2 renderer (hosted file; loaded from the hot-stove-gm page footer) */
(function () {
var D = null, team = null, S = null;
var POS = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH'];
var YRS = [2027, 2028, 2029, 2030, 2031];
var MEM = null; function canClear() { if (/member_status=free/.test(location.search)) return false; if (/member_status=(paid|comped)/.test(location.search)) return true; if (SRV.me) return !!SRV.me.paid; return (MEM && /^(paid|comped)$/.test(MEM.status || '')) || /member_status=(paid|comped)/.test(location.search); }
function isFree() { if (/member_status=free/.test(location.search)) return true; if (/member_status=(paid|comped)/.test(location.search)) return false; if (SRV.me) return !SRV.me.paid; return !!(MEM && MEM.status === 'free'); }
function freeTeam() {
if (SRV.ready && !/member_status=free/.test(location.search)) { var mine = SRV.plans.filter(function (p) { return p.season === SRV.me.season; }).sort(function (a, b) { return a.created_at < b.created_at ? -1 : 1; }); return mine.length ? mine[0].team : ''; }
try { return localStorage.getItem('hsgm_free_team') || ''; } catch (e) { return ''; }
}
// ---------- accounts: plans save to the member's Baseball Things account (Cloudflare Worker + D1) ----------
var API = 'https://hot-stove-gm.jason-a-churchill.workers.dev';
var SRV = { ready: false, me: null, plans: [], tok: null, state: '', busy: null, timer: null, err: '' };
function getTok(force) {
if (SRV.tok && !force) return Promise.resolve(SRV.tok);
return fetch('/members/api/session', { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.text() : ''; }).then(function (t) { SRV.tok = (t || '').trim() || null; return SRV.tok; });
}
function api(method, path, body, retried) {
return getTok(retried).then(function (t) {
if (!t) return { s: 401, j: { error: 'sign_in_required' } };
return fetch(API + path, { method: method, headers: { Authorization: 'Bearer ' + t, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined })
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { s: r.status, j: j }; }); });
}).then(function (res) { if (res.s === 401 && !retried) return api(method, path, body, true); return res; });
}
function srvInit() {
return Promise.all([api('GET', '/api/me'), api('GET', '/api/plans')]).then(function (a) {
if (a[0].s !== 200 || a[1].s !== 200) { SRV.err = 'Couldn\u2019t reach your account. Your plan is saved in this browser for now.'; return; }
SRV.me = a[0].j; SRV.plans = a[1].j.plans || []; SRV.ready = true; SRV.err = '';
if (team) { applyServer(); render(); }
}).catch(function () { SRV.err = 'Couldn\u2019t reach your account. Your plan is saved in this browser for now.'; if (team) render(); });
}
function planFor(tc) { return SRV.plans.filter(function (p) { return p.team === tc; }).sort(function (a, b) { return a.updated_at < b.updated_at ? 1 : -1; })[0] || null; }
function applyServer() {
var p = planFor(team); if (!p) { S._pid = null; return; }
var d = (p.data && p.data.S) || null, b = blank();
if (d) { S = d; Object.keys(b).forEach(function (k) { if (S[k] == null) S[k] = b[k]; }); }
S._pid = p.id;
if (p.status === 'locked') S.locked = { name: p.title || 'My plan', at: Date.parse(p.locked_at) || Date.now() }; else delete S.locked;
try { localStorage.setItem(key(), JSON.stringify(S)); } catch (e) { }
}
function planData() { var c = {}; Object.keys(S).forEach(function (k) { if (k !== 'locked' && k !== '_pid') c[k] = S[k]; }); return { v: 1, S: c }; }
function lockSummary() {
var md = model(), s26 = in26(), C = counts(md);
var r40 = Object.keys(md.mine).filter(function (id) { return is40(P(id), md, s26); });
var unh = Object.keys(md.mine).map(P).filter(function (p) { return (p.on40 || md.from[p.id] !== 'base') && p.noOpt && !s26[p.id]; }).map(function (p) { return p.id; });
var d = planData(); d.roster26 = Object.keys(s26); d.rotation = S.rot.slice(); d.roster40 = r40; d.pitcherCount = C.pit; d.unhandled = unh;
var PY = payroll(); d.payroll = PY.total; return d;
}
function setSaved(st) { SRV.state = st; var sv = $('#hsgm .hg-saved'); if (sv) sv.innerHTML = savedTxt(); }
function ensurePlan() {
if (S._pid) return Promise.resolve({ ok: true });
if (SRV.busy) return SRV.busy;
var tm = team;
SRV.busy = api('POST', '/api/plans', { team: tm, data: planData() }).then(function (res) {
SRV.busy = null;
if (res.s === 201) { SRV.plans.push(res.j.plan); if (res.j.quota) SRV.me = res.j.quota; if (team === tm) S._pid = res.j.plan.id; try { localStorage.setItem('hsgm_draft2_' + tm, JSON.stringify(S)); } catch (e) { } return { ok: true, created: true }; }
if (res.s === 403 && res.j.error === 'plan_limit_reached') { if (res.j.status) SRV.me = Object.assign(SRV.me || {}, res.j); return api('GET', '/api/plans').then(function (r2) { if (r2.s === 200) SRV.plans = r2.j.plans || []; render(); toast(lockMsg()); return { ok: false, limit: true }; }); }
return { ok: false };
}, function () { SRV.busy = null; return { ok: false }; });
return SRV.busy;
}
function syncSoon() {
if (!SRV.ready || S.locked || roFree()) return;
clearTimeout(SRV.timer); setSaved('saving');
SRV.timer = setTimeout(function () {
var tm = team;
ensurePlan().then(function (r) {
if (!r.ok) { setSaved(r.limit ? '' : 'error'); return; }
if (r.created) { setSaved('saved'); return; }
return api('PUT', '/api/plans/' + S._pid, { data: planData() }).then(function (res) {
if (res.s === 200) { var i = SRV.plans.findIndex(function (p) { return p.id === res.j.plan.id; }); if (i >= 0) SRV.plans[i] = res.j.plan; if (team === tm) setSaved('saved'); }
else if (res.s === 409) { api('GET', '/api/plans').then(function (r2) { if (r2.s === 200) { SRV.plans = r2.j.plans || []; applyServer(); render(); } }); }
else setSaved('error');
});
}, function () { setSaved('error'); });
}, 700);
}
function roFree() { var ft = freeTeam(); return isFree() && ft && ft !== team; }
var UPG = '#/portal/account/plans';
var UXCSS = "#hsgm .hg-ro button,#hsgm .hg-ro select,#hsgm .hg-ro input,#hsgm .hg-ro .slot{pointer-events:auto}#hsgm .hg-ro .slot{cursor:not-allowed}#hsgm .hg-ro [data-ptab],#hsgm .hg-ro [data-mp]{cursor:pointer}" + "#hsgm .hg-strip{gap:8px}#hsgm .hg-strip button{background:#eef3f9;border:2px solid transparent;border-radius:10px;padding:5px 5px 3px;opacity:1;display:flex;flex-direction:column;align-items:center;gap:1px;min-width:46px}#hsgm .hg-strip button span{font-size:10px;font-weight:800;color:#3a4d66;letter-spacing:.04em}#hsgm .hg-strip button:hover{background:#fff;border-color:var(--acc)}#hsgm .hg-strip button.on{background:#fff;border-color:var(--good);box-shadow:0 0 0 3px rgba(62,207,142,.35)}#hsgm .hg-strip img{width:28px;height:28px}#hsgm .hg-how{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px}#hsgm .hg-how h3{margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:.08em;color:var(--acc)}#hsgm .hg-howg{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}#hsgm .hg-howg b{display:block;color:#fff;font-size:15.5px;margin-bottom:4px}#hsgm .hg-howg span{color:var(--mute);font-size:14px}#hsgm .pl .nm[data-pc]{cursor:pointer;text-decoration:underline;text-decoration-color:rgba(255,255,255,.25);text-underline-offset:3px}#hsgm .pl .nm[data-pc]:hover{color:var(--acc);text-decoration-color:var(--acc)}#hsgm .hg-mw{position:relative;display:inline-block}#hsgm .hg-menu{display:none;position:absolute;right:0;top:calc(100% + 4px);z-index:40;background:#0d1f38;border:1px solid rgba(255,255,255,.18);border-radius:10px;padding:5px;min-width:230px;box-shadow:0 12px 30px rgba(0,0,0,.5)}#hsgm .hg-mw.open .hg-menu{display:flex;flex-direction:column;gap:2px}#hsgm .pl .acts .hg-menu button{background:none;border:0;text-align:left;padding:8px 10px;font-size:13.5px;border-radius:7px;width:100%}#hsgm .pl .acts .hg-menu button:hover{background:rgba(108,180,255,.15)}#hsgm .pl .acts button[data-more]{font-weight:700}#hsgm .hg-mw.open button[data-more]{border-color:var(--acc);color:var(--acc)}#hsgm .hg-needs{display:flex;flex-wrap:wrap;gap:6px;align-items:center;background:rgba(255,181,71,.08);border:1px solid rgba(255,181,71,.35);border-radius:10px;padding:8px 10px;margin:0 0 10px;font-size:13px}#hsgm .hg-needs b{color:var(--warn);margin-right:2px}#hsgm .hg-needs span{background:rgba(255,181,71,.14);color:#ffd9a0;border-radius:999px;padding:2px 9px;font-weight:600}#hsgm .hg-needs.ok{background:rgba(62,207,142,.1);border-color:rgba(62,207,142,.45)}#hsgm .hg-needs.ok b{color:var(--good)}#hsgm .hg-af{display:flex;gap:10px;align-items:center;margin:0 0 10px}#hsgm .hg-af span{color:var(--mute);font-size:12.5px;line-height:1.35}#hsgm .hg-af .hg-btn{white-space:nowrap;border-color:var(--acc);color:var(--acc);font-weight:700;border-radius:9px;padding:7px 12px}#hsgm .hg-ptabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin:0 0 10px}#hsgm .hg-ptabs button{background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:10px;padding:7px 8px;cursor:pointer;font-weight:800;font-size:14px;text-align:center;line-height:1.2}#hsgm .hg-ptabs button small{display:block;font-weight:600;font-size:11.5px;color:var(--mute);margin-top:2px}#hsgm .hg-ptabs button.on{background:var(--acc);color:#06121f;border-color:var(--acc)}#hsgm .hg-ptabs button.on small{color:#0b2a4a}#hsgm .hg-cmp{grid-template-columns:1fr!important;gap:12px}#hsgm .hg-cmp .slot{min-height:32px;padding:4px 8px;margin-bottom:4px}#hsgm .hg-cmp .slot.empty .who{font-weight:600;color:var(--mute)}#hsgm .hg-bud input{width:92px;background:#0a1830;color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:3px 7px;font-size:15px;font-weight:700;margin-top:2px}#hsgm .hg-bud input::placeholder{color:var(--mute);font-weight:600;font-size:13px}#hsgm .hg-okc{color:var(--good);font-weight:800}#hsgm .hg-badc{color:var(--bad);font-weight:800}#hsgm .hg-saved{align-self:center;color:var(--mute);font-size:12.5px;margin-left:4px}#hsgm .hg-mtog{display:none}#hsgm .hg-lkpv{background:#0a1628;border:1px solid var(--line);border-radius:10px;padding:8px;max-height:360px;overflow:auto;text-align:center;color:var(--mute)}#hsgm .hg-lkpv canvas{max-width:100%;height:auto;border-radius:6px}#hsgm .hg-unlock{background:#fff;color:#0b1628;border:0;border-radius:9px;padding:8px 14px;font-weight:800;font-size:14px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.3)}#hsgm .hg-unlock:hover{background:var(--warn)}#hsgm .hg-card{width:min(520px,calc(100vw - 24px))}#hsgm .hg-cardh{display:flex;gap:14px;align-items:center}#hsgm .hg-cardh img{width:64px;height:64px;border-radius:50%;object-fit:cover;background:#1d3a63;flex:none}#hsgm .hg-cardh h3{margin:0 0 4px}#hsgm .hg-cardf{display:flex;flex-wrap:wrap;gap:3px 7px;color:var(--mute);font-size:13px}#hsgm .hg-cardr{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:14px;padding:6px 0;border-bottom:1px solid var(--line)}#hsgm .hg-cardr b{min-width:70px;color:#fff}#hsgm .hg-cardc span b{min-width:0;color:var(--mute);margin-right:4px}#hsgm a.hg-pagelink{text-decoration:none;border-radius:9px;padding:7px 12px;border-color:var(--acc);color:var(--acc);font-weight:700}@media (max-width:900px){#hsgm .hg-howg{grid-template-columns:1fr 1fr}#hsgm .hg-mtog{display:grid;grid-template-columns:1fr 1fr;gap:6px;position:sticky;top:0;z-index:21;background:var(--bg);padding:6px 0;margin:-8px 0 10px}#hsgm .hg-mtog button{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:10px;padding:10px;font-weight:800;font-size:15px}#hsgm .hg-mtog button.on{background:var(--acc);color:#06121f;border-color:var(--acc)}#hsgm .hg-grid.mp-plan>.hg-box{display:none}#hsgm .hg-grid:not(.mp-plan)>.hg-side{display:none}#hsgm .hg-grid.noplan.mp-plan>.hg-side{display:block}#hsgm .hg-ptog{display:none}#hsgm .hg-pay{position:static}#hsgm .hg-list{max-height:none}}@media (max-width:600px){#hsgm .hg-howg{grid-template-columns:1fr}#hsgm .hg-af{flex-direction:column;align-items:flex-start}}";
var PLAN_OPEN = true; try { PLAN_OPEN = localStorage.getItem('hsgm_plan_open') !== '0'; } catch (e) { }
var view = { hand: '', psub: '', fsort: 'war', ts1: '40', ts2: '40', pf: 'all', mode: 'roster', rtab: '40', q: '', faq: '', fag: 'all', tq1: '', tq2: '', tp: '', tsend: {}, trecv: {}, tcash: { dir: 'none', amt: '', split: ['', '', '', '', ''] } };
function $(s, r) { return (r || document).querySelector(s); }
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); }
function M(v) { if (v == null) return '—'; if (v === 0) return '$0'; if (Math.abs(v) < 1e6) return (v < 0 ? '−' : '') + '$' + Math.round(Math.abs(v) / 1000) + 'K'; var m = v / 1e6, a = Math.abs(m); return (m < 0 ? '−' : '') + '$' + (a >= 100 ? a.toFixed(0) : a.toFixed(a % 1 === 0 ? 0 : (a < 10 ? 2 : 1))) + 'M'; }
function num(v) { var n = parseFloat(String(v).replace(/[$,\s]/g, '')); if (isNaN(n)) return 0; return n < 1000 ? Math.round(n * 1e6) : Math.round(n); } // "5" or "5.5" = $M
function age(dob) { if (!dob) return ''; var d = new Date(dob), n = new Date(2027, 3, 1); var a = n.getFullYear() - d.getFullYear(); if (n < new Date(n.getFullYear(), d.getMonth(), d.getDate())) a--; return a; }
function P(id) { var p = D.players[id]; if (p) p.id = +id; return p; }
function isP(p) { return p.pp === 'P' || p.pp === 'TWP' || ((p.el || []).indexOf('P') >= 0 && p.el.length === 1); }
function canHit(p) { return (p.el || []).some(function (x) { return x !== 'P'; }); }
function canPitch(p) { return (p.el || []).indexOf('P') >= 0; }
function tname(tc) { return D.teams[tc] ? D.teams[tc].name : tc; }
function key() { return 'hsgm_draft2_' + team; }
function blank() { return { tender: {}, option: {}, moves: [], seq: 1, lr: [], ll: [], br: [], bl: [], rot: [], pen: [] }; }
function load() { try { var s = localStorage.getItem(key()); S = s ? JSON.parse(s) : blank(); } catch (e) { S = blank(); } var b = blank(); Object.keys(b).forEach(function (k) { if (S[k] == null) S[k] = b[k]; }); Object.keys(S.tender).forEach(function (id) { var p = D.players[id]; if (p && p.optPending && S.option[id] !== 'dec') delete S.tender[id]; }); }
function save() { if (roFree()) { load(); return; } if (isFree() && !freeTeam()) { try { localStorage.setItem('hsgm_free_team', team); } catch (e) { } } try { localStorage.setItem(key(), JSON.stringify(S)); view.savedAt = Date.now(); } catch (e) { } var sv = $('#hsgm .hg-saved'); if (sv) sv.innerHTML = savedTxt(); syncSoon(); }
function savedTxt() {
if (SRV.ready) { if (SRV.state === 'saving') return 'Saving to your account\u2026'; if (SRV.state === 'error') return 'Couldn\u2019t save to your account. Saved in this browser; we\u2019ll retry on your next change.'; if (S && S._pid) return '\u2713 Saved to your account' + (view.savedAt ? ' ' + new Date(view.savedAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) : ''); return 'Your plan saves to your account as soon as you make a move.'; }
if (SRV.err) return '' + esc(SRV.err) + '';
return 'Connecting to your account\u2026';
}
function hasWork() { return S && (S.moves.length || Object.keys(S.tender).length || Object.keys(S.option).length || S.lr.length || S.ll.length || S.rot.length || S.pen.length || S.br.length || S.bl.length); }
// ---------- model: who's on my team in this plan, and the money ----------
function arbPending(p) { return p.arb && p.arb.a == null; }
function ctlDec(p) { return p.optPending && p.optPending.ctl && S.option[p.id] === 'dec'; }
function arbLive(p) { return arbPending(p) && (!p.optPending || ctlDec(p)); }
function optLeaves(p, id) { return S.option[id] === 'dec' && !(p.optPending && p.optPending.ctl); }
function arbProj(p) { return p.arb ? (p.arb.p || p.arb.c || 0) : 0; }
function contractCash(c, y) { var i = y - c.start; var s = (i >= 0 && i < c.sal.length) ? c.sal[i] : 0; var b = c.bonus ? (c.bonus[y - 2027] || 0) : 0; var last = c.start + c.sal.length - 1; var buy = (c.opt && c.opt.type && y === last) ? (c.opt.buy || 0) : 0; return s + b + buy; }
function model() {
var add40 = {}, mine = {}, from = {}, dfa = {}, sent = {}, contract = {}, cash27 = 0, cashTo = {}, tradeIn = {};
Object.keys(D.players).forEach(function (id) { var p = D.players[id]; if (p.t === team && !p.pendingFA) { mine[id] = 1; from[id] = 'base'; } });
Object.keys(S.tender).forEach(function (id) { if (S.tender[id] === 'nt') delete mine[id]; });
Object.keys(S.option).forEach(function (id) { var p = D.players[id]; if (p && optLeaves(p, id)) delete mine[id]; });
S.moves.forEach(function (m) {
if (m.k === 'sign') { mine[m.id] = 1; from[m.id] = 'fa'; contract[m.id] = m.c; }
else if (m.k === 'msign') { mine[m.id] = 1; from[m.id] = 'mfa'; }
else if (m.k === 'ext') { contract[m.id] = m.c; }
else if (m.k === 'dfa') { delete mine[m.id]; dfa[m.id] = 1; }
else if (m.k === 'add40') { add40[m.id] = m.mid; }
else if (m.k === 'trade') {
m.send.forEach(function (id) { delete mine[id]; sent[id] = m.tm; });
m.recv.forEach(function (id) { mine[id] = 1; if (!from[id] || from[id] !== 'base') from[id] = 'trade'; tradeIn[id] = m.tm; delete sent[id]; });
if (m.cash && m.cash.amt) { var v = m.cash.split[0] || 0; cash27 += m.cash.dir === 'send' ? v : -v; }
}
});
Object.keys(add40).forEach(function (id) { if (!mine[id]) delete add40[id]; });
return { add40: add40, mine: mine, from: from, dfa: dfa, sent: sent, contract: contract, cash27: cash27, tradeIn: tradeIn };
}
function is40(p, md, s26) { return (p.on40 && md.from[p.id] !== 'mfa') || md.from[p.id] === 'fa' || !!s26[p.id] || !!md.add40[p.id]; }
function in26() { var s = {}; S.lr.concat(S.ll).forEach(function (x) { s[x.id] = 1; }); S.br.concat(S.bl, S.rot, S.pen).forEach(function (id) { s[id] = 1; }); return s; }
// 2027 money a player carries on MY payroll in this plan (full amount, not a delta)
function money27(p, md, on26) {
var id = p.id, c = md.contract[id];
if (c) { var cc = contractCash(c, 2027); if (c.start > 2027) cc += (p.sal && p.sal[0] > 0 ? p.sal[0] : 0); return cc; }
if (p.optPending && !ctlDec(p)) return S.option[id] === 'up' ? (p.optPending.sal || 0) : 0;
if (p.sal && p.sal[0] > 0) return p.sal[0];
if (arbLive(p)) return S.tender[id] === 't' ? arbProj(p) : 0;
if (p.arb && p.arb.a) return p.arb.a;
return on26 ? D.leagueMin : 0;
}
function baseCount() {
var n = 0;
Object.keys(D.players).forEach(function (id) { var p = D.players[id]; if (p.t === team && p.sal && p.sal[0] > 0) n++; });
(D.dead[team] || []).forEach(function (d) { if (d[1][0] > 0 && !/buyout/i.test(d[2] || '')) n++; });
return n;
}
// Jason (Oct 27): option buyouts count toward 2026 payroll, never 2027. Cot's 2027 buyout rows come out of the starting number.
function buyout27(tc) { var t = 0; (D.dead[tc] || []).forEach(function (d) { if (/buyout/i.test(d[2] || '')) t += d[1][0] || 0; }); return t; }
function basePay(tc) { return (D.teams[tc].pay27 || 0) - buyout27(tc); }
function payroll() {
var md = model(), s26 = in26(), T = D.teams[team];
var base = basePay(team), n0 = baseCount();
var total = base, n = n0, br = { arb: [0, 0], opt: [0, 0], fa: [0, 0], trade: [0, 0], min: [0, 0], ext: [0, 0] };
function addTo(k, v, cnt) { br[k][0] += v; br[k][1] += cnt; total += v; n += cnt; }
// base players: start from what's already inside "committed"
Object.keys(D.players).forEach(function (id) {
var p = P(id); if (p.t !== team) return;
var inBase = (p.sal && p.sal[0] > 0) ? p.sal[0] : 0;
var now = md.mine[id] ? money27(p, md, !!s26[id]) : (md.dfa[id] ? inBase : 0);
if (p.optPending && !md.mine[id] && S.option[id] !== 'dec' && md.sent[id] && S.option[id] === 'up') now = 0;
var d = now - inBase;
if (!d) return;
var cnt = (now > 0 ? 1 : 0) - (inBase > 0 ? 1 : 0);
var k = md.sent[id] ? 'trade' : md.contract[id] ? 'ext' : (p.optPending && !(ctlDec(p) && S.tender[id])) ? 'opt' : arbPending(p) ? 'arb' : 'min';
addTo(k, d, cnt);
});
// players who joined in this plan (free agents, trades)
Object.keys(md.mine).forEach(function (id) {
var p = P(id); if (p.t === team) return;
var v = money27(p, md, !!s26[id]); if (!v) return;
var k = (md.from[id] === 'fa' || md.from[id] === 'mfa') ? 'fa' : 'trade';
if (md.from[id] === 'trade' && md.contract[id] && S.moves.some(function (m) { return m.k === 'ext' && m.id === +id; })) k = 'ext';
addTo(k, v, 1);
});
if (md.cash27) addTo('trade', md.cash27, 0);
var dead = 0; (D.dead[team] || []).forEach(function (d) { dead += d[1][0] || 0; });
return { base: base, n0: n0, total: total, n: n, br: br, dead: dead, md: md };
}
// ---------- counts / checks ----------
function counts(md) {
var s26 = in26(), ids = Object.keys(s26), pit = 0, forty = 0;
var pitch = {}; S.rot.concat(S.pen).forEach(function (id) { pitch[id] = 1; });
Object.keys(pitch).forEach(function (id) { var p = P(id); if (p && !p.tw) pit++; });
Object.keys(md.mine).forEach(function (id) { var p = P(id); if (is40(p, md, s26)) forty++; });
return { n26: ids.length, pit: pit, forty: forty, s26: s26 };
}
function checks(md) {
var c = counts(md), out = [];
function pl(n, w) { return n + ' ' + w + (n === 1 ? '' : 's'); }
var d26 = 26 - c.n26;
out.push([d26 === 0, d26 > 0 ? 'Add ' + pl(d26, 'more player') + ' to the 26-man (' + c.n26 + ' of 26)' : d26 < 0 ? 'Take ' + pl(-d26, 'player') + ' off the 26-man (' + c.n26 + ' of 26)' : '26-man roster: 26 of 26']);
out.push([c.forty <= 40, c.forty > 40 ? 'Clear ' + pl(c.forty - 40, '40-man spot') + ': trade, DFA or non-tender (' + c.forty + ' of 40)' : '40-man roster: ' + c.forty + ' of 40']);
var dr = 5 - S.rot.length;
out.push([dr === 0, dr > 0 ? 'Add ' + pl(dr, 'more starter') + ' to the rotation (' + S.rot.length + ' of 5)' : 'Rotation: 5 starters']);
out.push([c.pit <= 13, c.pit > 13 ? 'Take ' + pl(c.pit - 13, 'pitcher') + ' off the 26-man (' + c.pit + ' of 13 max)' : 'Pitchers on the 26-man: ' + c.pit + ' (max 13)']);
['lr', 'll'].forEach(function (k) {
var L = S[k], lbl = k === 'lr' ? 'lineup vs. RHP' : 'lineup vs. LHP', have = {}, dup = {};
L.forEach(function (x) { if (have[x.pos]) dup[x.pos] = 1; have[x.pos] = 1; });
var miss = POS.filter(function (q) { return !have[q]; }), dups = Object.keys(dup);
var ok = L.length === 9 && !dups.length && !miss.length, msg;
if (ok) msg = 'Full ' + lbl;
else if (L.length < 9) msg = 'Add ' + pl(9 - L.length, 'more hitter') + ' to the ' + lbl + (miss.length ? ' (still need ' + miss.join(', ') + ')' : '');
else msg = 'Fix the ' + lbl + ': ' + (dups.length ? dups.join(', ') + ' listed twice' : '') + (dups.length && miss.length ? '; ' : '') + (miss.length ? 'nobody at ' + miss.join(', ') : '');
out.push([ok, msg]);
});
var mineL = Object.keys(md.mine).map(P);
var stuck = mineL.filter(function (p) { return (p.on40 || md.from[p.id] !== 'base') && p.noOpt && !c.s26[p.id]; });
out.push([!stuck.length, stuck.length ? 'Out of options, so each needs a 26-man spot, a trade or a DFA: ' + stuck.map(function (p) { return p.n; }).join(', ') : 'Every out-of-options player is handled']);
var arbU = mineL.filter(function (p) { return arbLive(p) && !S.tender[p.id] && !md.contract[p.id]; });
out.push([!arbU.length, arbU.length ? 'Tender or non-tender ' + (arbU.length > 4 ? pl(arbU.length, 'player') + ' in the Arbitration tab' : arbU.map(function (p) { return p.n; }).join(', ')) : 'All arbitration decisions made']);
var optU = mineL.filter(function (p) { return p.optPending && !S.option[p.id]; });
out.push([!optU.length, optU.length ? 'Pick up or decline the option for ' + optU.map(function (p) { return p.n; }).join(', ') : 'All option decisions made']);
return out;
}
// ---------- prospect rankings: Jason's sheet for the Mariners, MLB Pipeline top 30 for everyone else ----------
var RK = {};
var SEA_SHEET = '1stxy35aQDJmFjVRpS0EGR9Ds9qT77HJuuRgYZHcWnvY';
function slugOf(tc) { return String((D.teams[tc] || {}).name || '').toLowerCase().replace(/[^a-z]/g, ''); }
function buildRanks() { var L = (D.pipe && D.pipe.lists) || {}; Object.keys(D.teams).forEach(function (tc) { var a = L[slugOf(tc)]; if (!a) return; var m = {}; a.forEach(function (id, i) { m[id] = i + 1; }); RK[tc] = m; }); }
function normN(n) { return String(n || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z ]/g, '').replace(/\s+(jr|sr|ii|iii|iv)$/, '').replace(/\s+/g, ' ').trim(); }
function parseCSV(t) { var rows = [], row = [], cur = '', q = false; for (var i = 0; i < t.length; i++) { var c = t[i]; if (q) { if (c === '"') { if (t[i + 1] === '"') { cur += '"'; i++; } else q = false; } else cur += c; } else if (c === '"') q = true; else if (c === ',') { row.push(cur); cur = ''; } else if (c === '\n' || c === '\r') { if (c === '\r' && t[i + 1] === '\n') i++; row.push(cur); rows.push(row); row = []; cur = ''; } else cur += c; } if (cur || row.length) { row.push(cur); rows.push(row); } return rows; }
function loadSEA() {
var pr = window.btSheet ? window.btSheet(SEA_SHEET, '0') : fetch('https://docs.google.com/spreadsheets/d/' + SEA_SHEET + '/export?format=csv&gid=0').then(function (r) { return r.text(); });
Promise.resolve(pr).then(function (t) {
if (!t) return; var rows = parseCSV(t), hdr = (rows[0] || []).map(function (x) { return x.trim().toUpperCase(); });
var ri = hdr.indexOf('RANK'), ni = hdr.indexOf('PLAYER'); if (ri < 0 || ni < 0) return;
var byName = {}; Object.keys(D.players).forEach(function (k) { if (D.players[k].t === 'SEA') byName[normN(D.players[k].n)] = +k; });
var m = {}, miss = [];
rows.slice(1).forEach(function (r) { var rk = parseInt(r[ri], 10); if (!(rk >= 1 && rk <= 30)) return; var k = byName[normN(r[ni])]; if (k) m[k] = rk; else miss.push(r[ni]); });
window.__HSGM_SEA_RANKS = { matched: Object.keys(m).length, missing: miss };
if (Object.keys(m).length >= 10) { RK.SEA = m; if (team) render(); }
}).catch(function () { });
}
function byRank(tc) { var m = RK[tc] || {}; return function (a, b) { var ra = m[a.id] || 999, rb = m[b.id] || 999; return ra - rb || byLast(a, b); }; }
// ---------- shell ----------
function shell() {
var root = $('#hsgm');
var logos = Object.keys(D.teams).sort(function (a, b) { return D.teams[a].name.localeCompare(D.teams[b].name); }).map(function (k) {
return '';
}).join('');
root.innerHTML = '
MLB · Front office
Hot Stove GM
Pick a team, make the offseason’s calls, build the Opening Day 26 and share your plan.
' + logos + '
' + howHTML() + '
';
root.querySelectorAll('.hg-strip button').forEach(function (b) { b.addEventListener('click', function () { pick(b.getAttribute('data-tm')); }); });
}
function howHTML() {
return '
How it works
' +
'
Pick a teamClick a logo above. You start from that team\u2019s real 40-man roster, prospects, contracts and 2027 payroll.
' +
'
Make the callsTender or non-tender arbitration players, pick up or decline options, trade, sign free agents, extend players and DFA. Go in any order and undo anything.
' +
'
Build the 26Fill The Plan: lineups against righties and lefties, the bench, a five-man rotation and the bullpen. Auto-fill gives you a starting point.
' +
'
Lock it in and shareWhen every item is done, name your plan, lock it and share a snapshot of every move.
' +
'
Click any player\u2019s name for his stats and contract.
\u2713 Locked: ' + esc(S.locked.name) + '' + new Date(S.locked.at).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) + '. A locked plan can\u2019t be edited.' + (canClear() ? '' : '') + '
' : '';
var freeBar = roFree() ? '
Free members get one plan, and yours is the ' + esc(tname(freeTeam())) + '.You can look around the ' + esc(T.name) + ', but to build plans for every team, plus clear and unlock them, upgrade to a paid membership.Upgrade
\u{1F512} This plan is locked, so it\u2019s view only.' + (canClear() ? ' To make changes, use \u201cUnlock to edit\u201d in the green bar above.' : ' Free plans stay locked. Upgrade to unlock it, clear it or build plans for other teams.') + '
' : roFree() ? '
\u{1F512} View only. Your free plan is the ' + esc(tname(freeTeam())) + '.
' : '') + left + '
The Plan
' + boardHTML(md, C) + '
Before you lock it in
' +
checks(md).map(function (c) { return '
' + (c[0] ? '✓ ' : '• ') + esc(c[1]) + '
'; }).join('') +
'
* Players without a contract count at the league minimum ($' + (D.leagueMin / 1000).toFixed(0) + 'K, the 2026 figure, until the 2027 minimum is set), and only while they’re on the 26-man. Your plan saves to your Baseball Things account automatically.
';
wire();
}
function ageNow(dob) { if (!dob) return 99; var d = new Date(dob), n = new Date(); var a = n.getFullYear() - d.getFullYear(); if (n < new Date(n.getFullYear(), d.getMonth(), d.getDate())) a--; return a; }
function r5(p) { return p.r5 === 1 && !p.on40 && (ageNow(p.dob) < 26 || !!(RK[p.t] && RK[p.t][p.id])); }
function tags(p, md) {
var t = '';
if (r5(p) && !(md && md.add40[p.id])) t += 'Rule 5 eligible ';
if (md && md.add40[p.id]) t += 'Added to 40-man ';
if (p.noOpt && (p.on40 || md.from[p.id] !== 'base')) t += 'Can’t option ';
if (arbLive(p) && !md.contract[p.id]) t += 'Arb ' + (p.arb.y || '') + ' ';
if (p.optPending) t += '' + esc(p.optPending.type.replace(/ OPTION/, '').toLowerCase()) + ' option ';
if (p.tw) t += 'Two-way ';
if (md.from[p.id] === 'fa') t += 'Signed ';
if (md.from[p.id] === 'mfa') t += 'Minor-league deal ';
if (md.from[p.id] === 'trade') t += 'Acquired ';
if (md.contract[p.id] && md.from[p.id] !== 'fa') t += 'New deal ';
return t;
}
function metaOf(p, md) {
var c = md && md.contract[p.id];
var sal = c ? contractCash(c, 2027) || null : (p.sal && p.sal[0] > 0 ? p.sal[0] : (p.optPending && !ctlDec(p) ? p.optPending.sal : (arbLive(p) ? arbProj(p) : null)));
var end = c ? c.start + c.sal.length - 1 : p.end;
return [posLab(p), (p.b || '?') + '/' + (p.th || '?'), age(p.dob) ? 'Age ' + age(p.dob) : '', p.mls ? 'MLS ' + p.mls : '', p.opt != null && p.on40 && !(p.mls && parseFloat(p.mls) >= 5) ? 'Options ' + p.opt : '', sal ? '2027 ' + M(sal) + (arbLive(p) && !c ? ' (proj.)' : '') : '', end ? 'Through ' + end : ''].filter(Boolean);
}
function lockMsg() {
if (roFree()) return 'View only. Your free plan is the ' + tname(freeTeam()) + '. Upgrade to build plans for other teams.';
if (canClear()) return 'This plan is locked. Click \u201cUnlock to edit\u201d in the green bar to make changes.';
return 'This plan is locked for good. Free plans can\u2019t be unlocked or cleared. Upgrade to unlock it and make changes.';
}
var VIEW_OK = '[data-ptab],[data-rtab],[data-pf],[data-ps],[data-hd],[data-fs],[data-ts],select[data-tp],[data-fl],[data-r5only],[data-pc],[data-clrf],[data-mode-go],[data-mp],[data-snap],.hg-ronote a';
function nmH(p) { return '' + esc(p.n) + ''; }
function moreMenu(items) { return items ? '' + items + '' : ''; }
function plRow(p, md, s26, extraBtns, noBoard) {
var acts = '';
if (!noBoard && md.mine[p.id]) {
acts += '';
}
acts += extraBtns || '';
return '
' + nmH(p) + ' ' + tags(p, md) + '
' + acts + '
' + metaOf(p, md).map(esc).join('·') + '
';
}
function lastKey(n) { var w = String(n || '').replace(/,?\s+(Jr\.?|Sr\.?|II|III|IV|V)$/i, '').trim().split(/\s+/); var ln = w.pop() || ''; while (w.length && /^(de|del|la|las|los|van|von|der|da|di|le|st\.?)$/i.test(w[w.length - 1])) ln = w.pop() + ' ' + ln; return (ln + ' ' + w.join(' ')).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); }
function byLast(a, b) { return lastKey(a.n).localeCompare(lastKey(b.n)); }
function sortSal(md) { return function (a, b) { return (money27(b, md, false) || 0) - (money27(a, md, false) || 0) || a.n.localeCompare(b.n); }; }
function search(list, q) { q = (q || '').toLowerCase(); return q ? list.filter(function (p) { return p.n.toLowerCase().indexOf(q) >= 0; }) : list; }
function role(p) { var g = p.fg && p.fg.p; if (g && g[4] >= 5) return g[3] * 2 >= g[4] ? 'SP' : 'RP'; return (p.sp || 0) >= 50 ? 'SP' : 'RP'; }
function posLab(p) { if (isP(p) && !p.tw) return p.fg && p.fg.p && p.fg.p[4] >= 5 ? role(p) : (p.pp || 'P'); var L = (p.p26 || []).slice(); if (p.pp && L.indexOf(p.pp) > 0) { L.splice(L.indexOf(p.pp), 1); L.unshift(p.pp); } return L.length ? L.join('/') : (p.pp || ''); }
function posGrp(p) { if (p.tw) return ['P', 'SP', 'DH']; if (isP(p)) return ['P', role(p)]; var q = p.pp || ''; if (q === 'C') return ['C']; if (/^(1B|2B|3B|SS|IF)$/.test(q)) return ['IF']; if (/^(LF|CF|RF|OF)$/.test(q)) return ['OF']; return ['DH']; }
function handOK(p) { if (!view.hand) return true; var pit = view.pf === 'SP' || view.pf === 'RP'; return pit ? p.th === view.hand : p.b === view.hand; }
function pfilt(list) { list = list.filter(handOK); return view.pf === 'all' ? list : list.filter(function (p) { return posGrp(p).indexOf(view.pf) >= 0 || (view.psub && (p.p26 || []).indexOf(view.psub) >= 0); }).filter(function (p) { return !view.psub || (p.p26 || []).indexOf(view.psub) >= 0; }); }
function posChips() { var SUB = { IF: ['1B', '2B', '3B', 'SS'], OF: ['LF', 'CF', 'RF'] }[view.pf]; return '
' + pit.map(function (p) { return plRow(p, md, s26, btn(p)); }).join('') : '');
} else if (view.rtab === 'org') {
var O = pfilt(search(mine.filter(function (p) { return !is40(p, md, s26) && (!view.r5only || r5(p)); }), view.q)).sort(byRank(team));
html += '
' + O.length + ' players: the top 30 in ranking order' + (team === 'SEA' ? ' (Baseball Things rankings)' : ' (MLB Pipeline)') + ', then everyone else by last name. Putting one on the 26-man adds him to the 40-man.
Leave them off the 40-man and they can be taken in the Rule 5 draft.
';
html += O.map(function (p) { return plRow(p, md, s26, (r5(p) ? '' : '') + moreMenu((r5(p) ? '' : '') + '')); }).join('');
} else {
var gone = [];
Object.keys(D.players).forEach(function (id) { var p = P(id); if (p.t === team && (p.pendingFA || S.tender[id] === 'nt' || optLeaves(p, id))) gone.push(p); });
Object.keys(md.sent).forEach(function (id) { gone.push(P(id)); });
Object.keys(md.dfa).forEach(function (id) { gone.push(P(id)); });
gone = search(gone, view.q).sort(byLast);
html = '
Players off your roster in this plan. Undo puts a decision back.
Tender adds his projected 2027 salary to your committed payroll. Non-tender makes him a free agent you could sign back. Change your mind any time until the real decision comes in.
' + (rows || '
No arbitration-eligible players.
') + '
';
}
function optHTML(md) {
var all = Object.keys(D.players).map(P).filter(function (p) { return p.optPending && (p.t === team || md.mine[p.id]) && !md.sent[p.id]; }).sort(byLast);
var rows = all.map(function (p) {
var v = S.option[p.id] || '';
return '
Pick up adds the option salary. Decline means you owe the buyout; he becomes a free agent, or goes to arbitration if he’s still under team control (decide on him in the Arbitration tab). These go away once the real decision is made.
' + (rows || '
No pending 2027 options on this team.
') + '
';
}
function faPool(md) {
var ids = {}; (view.falvl === 'milb' ? (D.mfa || []) : D.fa).forEach(function (id) { ids[id] = 1; });
if (view.falvl === 'milb') return Object.keys(ids).filter(function (id) { return !md.mine[id] && D.players[id]; }).map(P);
Object.keys(D.players).forEach(function (id) { var p = D.players[id]; if (p.t === team && (S.tender[id] === 'nt' || optLeaves(p, id))) ids[id] = 1; });
return Object.keys(ids).filter(function (id) { return !md.mine[id] && D.players[id]; }).map(P);
}
function faHTML(md) {
var s26 = in26(), L = pfilt(search(faPool(md), view.faq)), pit = view.pf === 'SP' || view.pf === 'RP', hit = !pit && view.pf !== 'all';
var opts = [['name', 'Name'], ['war', 'fWAR']].concat(pit ? [] : [['wrc', 'wRC+']]).concat(hit ? [] : [['wpa', 'WPA']]);
if (!opts.some(function (o) { return o[0] === view.fsort; })) view.fsort = 'name';
var fb = function (p) { return p.fg && p.fg.b; }, fp = function (p) { var q = p.fg && p.fg.p; return q && q[2] >= 5 ? q : null; };
var war = function (p) { var b = fb(p), q = fp(p); return (b || q) ? (b ? b[0] || 0 : 0) + (q ? q[0] || 0 : 0) : -99; };
var wrc = function (p) { var b = fb(p); return b && b[1] != null ? (b[2] >= 100 ? 1e4 : 0) + b[1] : -1e4; };
var wpa = function (p) { var q = fp(p); return q && q[1] != null ? q[1] : -99; };
var key = { war: war, wrc: wrc, wpa: wpa }[view.fsort];
L.sort(key ? function (a, b) { return key(b) - key(a) || byLast(a, b); } : byLast);
var line = function (p) { var b = fb(p), q = fp(p), o = []; if (b && b[2] && (!q || b[2] >= 20)) o.push(b[0] + ' fWAR, ' + (b[1] == null ? '\u2014' : b[1]) + ' wRC+ (' + b[2] + ' PA)'); if (q && q[2]) o.push(q[0] + ' fWAR, ' + q[1] + ' WPA (' + q[2] + ' IP, ' + (q[3] * 2 >= q[4] ? q[3] + ' GS' : q[4] + ' G') + ')'); return o.length ? '2026: ' + o.join('; ') : ''; };
var sortBar = '
A major league deal puts him on the 40-man and your payroll. A minor league deal doesn\u2019t; he joins your prospects until you put him on the 26-man.
' + posChips() + sortBar + '
' + top + '
Available (' + L.length + ')
' + (rows || '
Nobody matches these filters' + (view.faq ? ' and the search "' + esc(view.faq) + '"' : '') + '.
') + '
';
}
function dealText(c) { var n = c.sal.length, tot = 0; for (var y = 2027; y <= c.start + n - 1; y++) tot += contractCash(c, y); return n + ' yr' + (n > 1 ? 's' : '') + ', ' + M(tot) + (c.opt && c.opt.type ? ' + ' + c.opt.type + ' option' : ''); }
function tradeHTML(md) {
var s26 = in26();
var teams = Object.keys(D.teams).filter(function (k) { return k !== team; }).sort(function (a, b) { return tname(a).localeCompare(tname(b)); });
var sel = '';
var done = S.moves.filter(function (m) { return m.k === 'trade'; }).map(function (m) { return '
With the ' + esc(tname(m.tm)) + '
' + esc(tradeText(m)) + '
'; }).join('');
var html = '
Trades
' + (done ? '
Trades in this plan
' + done : '') + '
New trade
' + sel;
if (!view.tp && Object.keys(view.tsend).length) html += '
Trading ' + Object.keys(view.tsend).map(function (id) { return esc(P(id).n); }).join(', ') + '. Pick a partner to see what you could get back.
';
if (view.tp) {
html += posChips();
var s26t = in26();
var my40 = function (p) { return is40(p, md, s26t); };
var mine = pfilt(search(Object.keys(md.mine).map(P).filter(function (p) { return view.ts1 === '40' ? my40(p) : !my40(p); }), view.tq1)).sort(view.ts1 === '40' ? byLast : byRank(team));
var theirs = pfilt(search(Object.keys(D.players).map(P).filter(function (p) { return p.t === view.tp && !p.pendingFA && !md.mine[p.id] && !md.sent[p.id] && (view.ts2 === '40' ? !!p.on40 : !p.on40); }), view.tq2)).sort(view.ts2 === '40' ? byLast : byRank(view.tp));
var tsw = function (n) { var v = view['ts' + n]; return ''; };
var row = function (p, side) { var on = (side === 's' ? view.tsend : view.trecv)[p.id]; return ''; };
var sendSel = Object.keys(view.tsend).map(P), recvSel = Object.keys(view.trecv).map(P);
var cs = view.tcash;
var eff = tradeEffect(md, sendSel, recvSel, cs);
html += '
Puts your 40-man players who played the most in 2026 into the open lineup, bench, rotation and bullpen spots. You can change anyone after.
' : '';
if (!RO && view.afPrev) af += '';
var tabs = '';
var body = t === 'p' ? listHTML('rot', 'Starting rotation', 5) + listHTML('pen', 'Bullpen') : t === 'l' ? lineupHTML('ll', 'Lineup vs. LHP') + listHTML('bl', 'Bench vs. LHP') : lineupHTML('lr', 'Lineup vs. RHP') + listHTML('br', 'Bench vs. RHP');
return head + af + tabs + '
' + body + '
';
}
// ---------- contract builder (free agents, extensions, multi-year deals) ----------
function openContract(id, kind) {
var p = P(id), md = model();
var cur = md.contract[id];
var hasDeal = !cur && p.sal && p.end && p.end >= 2027 && p.sal[0] > 0;
var start = kind === 'sign' ? 2027 : (hasDeal ? p.end + 1 : (p.optPending && S.option[id] === 'up' ? 2028 : 2027));
var st = { id: +id, kind: kind, start: start, n: 1, total: '', touched: false, sal: [''], bonus: '', bsplit: [], opt: '', osal: '', obuy: '', outs: {} };
if (cur) { st.start = cur.start; st.n = cur.sal.length; st.sal = cur.sal.map(function (v) { return String(v / 1e6); }); st.bonus = cur.bonusTot ? String(cur.bonusTot / 1e6) : ''; st.bsplit = (cur.bonus || []).map(function (v) { return v ? String(v / 1e6) : ''; }); st.opt = cur.opt ? cur.opt.type : ''; st.osal = cur.opt ? String(cur.opt.sal / 1e6) : ''; st.obuy = cur.opt ? String(cur.opt.buy / 1e6) : ''; (cur.outs || []).forEach(function (y) { st.outs[y] = 1; }); st.touched = true; var g = cur.sal.reduce(function (a, b) { return a + b; }, 0) + (cur.bonusTot || 0) + (cur.opt ? cur.opt.buy || 0 : 0); st.total = String(Math.round(g / 1e3) / 1e3); }
view.ct = st; drawContract();
}
function ctSpread(st) { var t = num(st.total || '') - num(st.bonus || '') - (st.opt ? num(st.obuy || '') : 0); if (!(t > 0)) return; var per = Math.floor(t / st.n / 1e3) * 1e3, a = []; for (var i = 0; i < st.n; i++) a.push(String((i === st.n - 1 ? t - per * (st.n - 1) : per) / 1e6)); st.sal = a; }
function ctYears(st) { var y = []; for (var i = 0; i < st.n; i++) y.push(st.start + i); return y; }
function ctBuild(st) {
var p = P(st.id), yrs = ctYears(st), last = yrs[yrs.length - 1];
var sal = yrs.map(function (y, i) { return num(st.sal[i] || ''); });
var bTot = num(st.bonus || ''), byears = []; for (var y = 2027; y <= last; y++) byears.push(y);
var bonus = byears.map(function (y, i) { return num(st.bsplit[i] || ''); });
var errs = [];
var bSum = bonus.reduce(function (a, b) { return a + b; }, 0);
if (bTot && Math.abs(bSum - bTot) > 1000) errs.push('The signing bonus split has to add up to ' + M(bTot) + '.');
yrs.forEach(function (y, i) { var b = bonus[y - 2027] || 0; if (sal[i] + b < D.leagueMin) errs.push(y + ': at least the league minimum (' + M(D.leagueMin) + ') per year.'); });
var G = num(st.total || ''), alloc = sal.reduce(function (a, b) { return a + b; }, 0) + bTot + (st.opt ? num(st.obuy || '') : 0);
if (!G) errs.unshift('Enter the total guaranteed dollars.'); else if (Math.abs(alloc - G) > 1000) errs.unshift('The years' + (bTot ? ', bonus' : '') + (st.opt ? ' and buyout' : '') + ' add up to ' + M(alloc) + '; the total guaranteed is ' + M(G) + '. They have to match.');
var opt = null;
if (st.opt) { opt = { type: st.opt, year: last + 1, sal: num(st.osal || ''), buy: num(st.obuy || '') }; if (!st.osal) errs.push('Enter the option salary.'); if (st.obuy === '') errs.push('Enter the option buyout (0 if none).'); }
var outs = Object.keys(st.outs).filter(function (y) { return st.outs[y] && +y < last; }).map(Number);
var label = '';
if (st.kind !== 'sign') label = last > (p.fa || 0) ? 'ext' : 'multi';
return { c: { start: st.start, sal: sal, bonus: bonus, bonusTot: bTot, opt: opt, outs: outs, kind: label }, errs: errs, label: label, last: last, alloc: alloc };
}
function drawContract() {
var st = view.ct, box = $('#hg-modal'); if (!st) { box.innerHTML = ''; return; }
var p = P(st.id), b = ctBuild(st), yrs = ctYears(st), byears = []; for (var y = 2027; y <= b.last; y++) byears.push(y);
var tot = 0; for (var y2 = 2027; y2 <= b.last + 0; y2++) tot += contractCash(b.c, y2);
var title = st.kind === 'sign' ? 'Sign ' + p.n : 'New deal for ' + p.n;
var info = st.kind === 'sign' ? '' : '
Free agent after ' + (p.fa || '?') + ' (service time ' + (p.mls || '0.000') + '). ' + (st.start > 2027 ? 'His current deal runs through ' + (st.start - 1) + ', so new years start in ' + st.start + '. ' : '') + 'A deal that runs past ' + (p.fa || '?') + ' is an extension; one that ends by then is a multi-year deal that buys out arbitration years.
' + (canClear() ? 'Once it’s locked, it can’t be edited until you unlock it. You can still view it and share it.' : 'Free members get one plan, and locking it is final: it can’t be unlocked, cleared or edited. You can still view it and share it. Upgrade to unlock and clear plans, and to build one for every team.') + '
' : '
Finish the items above, then come back to lock it in.
') + (ok ? '
Preview of what you\u2019ll share
Drawing\u2026
' : '') +
'
' + (ok ? ' ' : '') + '
';
var box = $('#hg-modal'); box.innerHTML = h;
box.querySelectorAll('[data-close]').forEach(function (e) { e.onclick = function () { box.innerHTML = ''; }; });
var nm = box.querySelector('[data-lkname]'), go = box.querySelector('[data-lkok]');
if (nm) { nm.focus(); nm.oninput = function () { st.name = nm.value; go.disabled = !nm.value.trim(); }; }
if (go) go.onclick = function () {
var nmv = st.name.trim();
if (!SRV.ready) { toast(SRV.err ? 'We can\u2019t reach your account right now, so the plan can\u2019t be locked yet. Try again in a minute.' : 'Still connecting to your account. Try again in a moment.'); return; }
go.disabled = true; go.textContent = 'Locking\u2026';
ensurePlan().then(function (r) {
if (!r.ok) { go.disabled = false; go.textContent = 'Lock It In'; if (!r.limit) toast('Couldn\u2019t reach your account. Try again.'); return; }
return api('POST', '/api/plans/' + S._pid + '/lock', { data: lockSummary(), title: nmv }).then(function (res) {
if (res.s === 200) { var i = SRV.plans.findIndex(function (p) { return p.id === res.j.plan.id; }); if (i >= 0) SRV.plans[i] = Object.assign(SRV.plans[i], res.j.plan); S.locked = { name: nmv, at: Date.now() }; view.lk = null; box.innerHTML = ''; try { localStorage.setItem(key(), JSON.stringify(S)); } catch (e) { } render(); toast('Locked. Your plan is set.'); }
else { go.disabled = false; go.textContent = 'Lock It In'; toast(res.j && res.j.problems ? res.j.problems.join(' ') : res.s === 409 ? 'This plan is already locked.' : 'Couldn\u2019t lock it. Try again.'); }
});
});
};
var pv = box.querySelector('[data-lkpv]');
if (pv) { var draw = function () { loadLogo(D.teams[team].id).then(function (logo) { var c; try { c = drawSnap('port', logo); c.toDataURL(); } catch (er) { c = drawSnap('port', null); } c.className = 'hg-snapc'; pv.innerHTML = ''; pv.appendChild(c); }); }; draw(); var tmr = null; if (nm) nm.addEventListener('input', function () { clearTimeout(tmr); tmr = setTimeout(draw, 400); }); }
}
// ---------- snapshot PNG ----------
var SNAP_FONT = '"Inter","Helvetica Neue",Arial,sans-serif';
function snapData() {
var md = model(), PY = payroll(), T = D.teams[team];
var lu = function (k) { return S[k].map(function (x, i) { var p = P(x.id); return [String(i + 1), p.n, x.pos, p.b || '']; }); };
var nm = function (L) { return L.map(function (id, i) { var p = P(id); return [String(i + 1), p.n, posLab(p), p.b || '']; }); };
var pit = function (L) { return L.map(function (id, i) { var p = P(id); return [String(i + 1), p.n, (p.th || '') + 'HP']; }); };
return { name: S.locked ? S.locked.name : (view.lk && view.lk.name.trim()) || 'My plan', team: T.full || T.name, tid: T.id, pay: M(PY.total) + ' (' + PY.n + ')',
lr: lu('lr'), ll: lu('ll'), br: nm(S.br), bl: nm(S.bl), rot: pit(S.rot), pen: pit(S.pen), groups: movesGrouped(md) };
}
function wrapLines(ctx, text, w) { var words = String(text).split(' '), out = [], line = ''; words.forEach(function (wd) { var t = line ? line + ' ' + wd : wd; if (ctx.measureText(t).width > w && line) { out.push(line); line = wd; } else line = t; }); if (line) out.push(line); return out; }
function drawSnap(shape, logo) {
var d = snapData(), W = shape === 'land' ? 1600 : 1080, PAD = 48;
var c = document.createElement('canvas'), ctx = c.getContext('2d');
function F(sz, wt) { ctx.font = (wt || 600) + ' ' + sz + 'px ' + SNAP_FONT; }
// layout pass (measure) then paint pass
function layout(paint) {
var y = 0, colGap = 36;
if (paint) { var g = ctx.createLinearGradient(0, 0, W, 200); g.addColorStop(0, '#163a63'); g.addColorStop(1, '#0a1c33'); ctx.fillStyle = '#0b1628'; ctx.fillRect(0, 0, W, c.height); ctx.fillStyle = g; ctx.fillRect(0, 0, W, 190); }
var lx = PAD; if (paint && logo) { ctx.drawImage(logo, PAD, 40, 110, 110); } lx = PAD + (logo ? 134 : 0);
if (paint) {
ctx.fillStyle = '#6cb4ff'; F(20, 800); ctx.fillText('HOT STOVE GM · 2026-2027 OFFSEASON PLAN', lx, 62);
ctx.fillStyle = '#ffffff'; F(shape === 'land' ? 50 : 46, 800); ctx.fillText(d.name, lx, 116, W - lx - 330);
ctx.fillStyle = '#d6e2f1'; F(24, 600); ctx.fillText(d.team, lx, 154);
ctx.textAlign = 'right'; ctx.fillStyle = '#93a7c2'; F(18, 800); ctx.fillText('COMMITTED 2027', W - PAD, 78); ctx.fillStyle = '#ffffff'; F(40, 800); ctx.fillText(d.pay, W - PAD, 124); ctx.textAlign = 'left';
}
y = 230;
function head(t, x, yy) { if (paint) { ctx.fillStyle = '#6cb4ff'; F(20, 800); ctx.fillText(t, x, yy); } return yy + 18; }
function rowsBlock(title, rows, x, w, yy, kind) {
yy = head(title, x, yy);
rows.forEach(function (r) {
yy += 36;
if (paint) {
ctx.fillStyle = '#132a49'; ctx.fillRect(x, yy - 26, w, 32);
ctx.fillStyle = '#93a7c2'; F(18, 800); ctx.fillText(r[0], x + 10, yy - 3);
ctx.fillStyle = '#ffffff'; F(21, 700); ctx.fillText(r[1], x + 40, yy - 3, w - 150);
ctx.textAlign = 'right'; ctx.fillStyle = '#93a7c2'; F(18, 700); ctx.fillText(kind === 'lu' ? r[2] + ' ' + r[3] : r[2], x + w - 10, yy - 3); ctx.textAlign = 'left';
}
});
return yy + 48;
}
function textBlock(title, items, x, w, yy, sz, sub) {
if (sub) { if (paint) { ctx.fillStyle = '#93a7c2'; F(16, 800); ctx.fillText(title, x, yy); } yy += 4; } else yy = head(title, x, yy) + 6; F(sz || 20, 600);
var lines = items.length ? [] : ['—'];
items.forEach(function (it) { wrapLines(ctx, it, w).forEach(function (ln) { lines.push(ln); }); });
lines.forEach(function (ln) { yy += (sz || 20) + 10; if (paint) { ctx.fillStyle = '#e8f0fa'; F(sz || 20, 600); ctx.fillText(ln, x, yy); } });
return yy + (sub ? 22 : 36);
}
function section(b, x, w, yy, sz, rule) {
if (!b) return yy;
if (rule && paint) { ctx.fillStyle = 'rgba(255,255,255,.14)'; ctx.fillRect(x, yy - 30, w, 1); }
if (paint) { ctx.fillStyle = '#6cb4ff'; F(19, 800); ctx.fillText(b.title, x, yy); } yy += 10;
b.subs.forEach(function (sb) {
if (sb[0]) { yy += 32; if (paint) { ctx.fillStyle = '#93a7c2'; F(15, 800); ctx.fillText(sb[0], x, yy); } yy += 2; }
F(sz, 600); sb[1].forEach(function (it) { wrapLines(ctx, it, w).forEach(function (ln, k) { yy += sz + 9; if (paint) { ctx.fillStyle = '#e8f0fa'; F(sz, 600); ctx.fillText(ln, x + (k ? 14 : 0), yy); } }); });
});
return yy + 60;
}
function movesGrid(x, w, yy, sz, cols) {
var g = d.groups, any = g.arb || g.opt || g.fa || g.ext || g.trade || g.dfa || g.add || g.mi;
if (paint) { ctx.fillStyle = '#6cb4ff'; F(28, 800); ctx.fillText('THE MOVES', x, yy + 6); } yy += 50;
if (!any) { if (paint) { ctx.fillStyle = '#93a7c2'; F(sz, 600); ctx.fillText('No moves', x, yy); } return yy + 30; }
var gap = 40, cw = (w - gap * (cols - 1)) / cols, X = function (i) { return x + i * (cw + gap); };
var colY = [], c;
for (c = 0; c < cols; c++) colY.push(yy);
[g.arb, g.opt, g.trade, g.fa, g.mi, g.add, g.ext, g.dfa].filter(Boolean).forEach(function (b, n) {
var k = 0;
if (n < cols) k = n; else for (c = 1; c < cols; c++) if (colY[c] < colY[k]) k = c;
colY[k] = section(b, X(k), cw, colY[k], sz, n >= cols);
});
return Math.max.apply(null, colY);
}
var benchTxt = function (L) { return L.length ? [L.map(function (b) { return b[0] + ' (' + b[1] + ')'; }).join(', ')] : []; };
if (shape === 'land') {
var cw = (W - PAD * 2 - colGap * 2) / 3, x1 = PAD, x2 = PAD + cw + colGap, x3 = PAD + (cw + colGap) * 2;
var a = rowsBlock('LINEUP VS. RHP', d.lr, x1, cw, y, 'lu'); a = rowsBlock('BENCH VS. RHP', d.br, x1, cw, a, 'lu');
var b = rowsBlock('LINEUP VS. LHP', d.ll, x2, cw, y, 'lu'); b = rowsBlock('BENCH VS. LHP', d.bl, x2, cw, b, 'lu');
var cc = rowsBlock('ROTATION', d.rot, x3, cw, y); cc = rowsBlock('BULLPEN', d.pen, x3, cw, cc);
y = Math.max(a, b, cc) + 10;
y = movesGrid(PAD, W - PAD * 2, y, 18, 3);
} else {
var hw = (W - PAD * 2 - colGap) / 2, xa = PAD, xb = PAD + hw + colGap;
var a2 = rowsBlock('LINEUP VS. RHP', d.lr, xa, hw, y, 'lu'), b2 = rowsBlock('LINEUP VS. LHP', d.ll, xb, hw, y, 'lu');
y = Math.max(a2, b2) + 6;
var a3 = rowsBlock('BENCH VS. RHP', d.br, xa, hw, y, 'lu'), b3 = rowsBlock('BENCH VS. LHP', d.bl, xb, hw, y, 'lu');
y = Math.max(a3, b3) + 6;
var a4 = rowsBlock('ROTATION', d.rot, xa, hw, y), b4 = rowsBlock('BULLPEN', d.pen, xb, hw, y);
y = Math.max(a4, b4) + 10;
y = movesGrid(PAD, W - PAD * 2, y, 19, 2);
}
y += 10;
if (paint) { ctx.fillStyle = '#10213a'; ctx.fillRect(0, c.height - 70, W, 70); ctx.fillStyle = '#ffffff'; F(24, 800); ctx.fillText('BASEBALL THINGS', PAD, c.height - 26); ctx.textAlign = 'right'; ctx.fillStyle = '#6cb4ff'; F(20, 700); ctx.fillText('baseballthings.com/hot-stove-gm', W - PAD, c.height - 27); ctx.textAlign = 'left'; }
return y + 70;
}
c.width = W; c.height = 10; var need = layout(false);
c.height = Math.max(shape === 'land' ? 900 : 1350, Math.ceil(need)); layout(true);
return c;
}
function loadLogo(tid) { return new Promise(function (res) { var im = new Image(); im.crossOrigin = 'anonymous'; im.onload = function () { res(im); }; im.onerror = function () { res(null); }; im.src = 'https://www.mlbstatic.com/team-logos/' + tid + '.svg'; setTimeout(function () { res(null); }, 4000); }); }
function openSnap() {
var box = $('#hg-modal'), shape = view.snapShape || 'port';
box.innerHTML = '
Snapshot
Drawing…
Post it:
X, Bluesky and Threads open a post with the text filled in; attach the PNG you downloaded or copied.
';
box.querySelectorAll('[data-close]').forEach(function (e) { e.onclick = function () { box.innerHTML = ''; }; });
box.querySelectorAll('[data-sh]').forEach(function (e) { e.onclick = function () { view.snapShape = e.getAttribute('data-sh'); openSnap(); }; });
loadLogo(D.teams[team].id).then(function (logo) {
var c; try { c = drawSnap(shape, logo); c.toDataURL(); } catch (er) { c = drawSnap(shape, null); }
var v = box.querySelector('.hg-snapv'); if (!v) return; v.innerHTML = ''; c.className = 'hg-snapc'; v.appendChild(c);
var fname = 'hot-stove-gm-' + team.toLowerCase() + '-' + shape + '.png';
box.querySelector('[data-dl]').onclick = function () { var a = document.createElement('a'); a.download = fname; a.href = c.toDataURL('image/png'); document.body.appendChild(a); a.click(); a.remove(); };
box.querySelector('[data-cp]').onclick = function () { c.toBlob(function (bl) { try { navigator.clipboard.write([new ClipboardItem({ 'image/png': bl })]).then(function () { toast('Image copied.'); }, function () { toast('Your browser blocked copying. Use Download instead.'); }); } catch (er) { toast('Your browser can’t copy images. Use Download instead.'); } }, 'image/png'); };
c.toBlob(function (bl) { var fl = new File([bl], fname, { type: 'image/png' }), sb = box.querySelector('[data-shr]'); if (sb && navigator.canShare && navigator.canShare({ files: [fl] })) { sb.style.display = ''; sb.onclick = function () { navigator.share({ files: [fl], text: shareText() }).catch(function () { }); }; } }, 'image/png');
});
box.querySelectorAll('[data-so]').forEach(function (e) { e.onclick = function () { var t = encodeURIComponent(shareText()), k = e.getAttribute('data-so'); var u = k === 'x' ? 'https://twitter.com/intent/tweet?text=' + t : k === 'bsky' ? 'https://bsky.app/intent/compose?text=' + t : 'https://www.threads.net/intent/post?text=' + t; window.open(u, '_blank', 'noopener'); }; });
}
function shareText() { return 'My ' + (D.teams[team].name) + ' 2026-2027 offseason plan: ' + (S.locked ? S.locked.name : '') + '. Built with Hot Stove GM from Baseball Things. baseballthings.com/hot-stove-gm'; }
// ---------- board actions ----------
function firstOpen(k, p) {
var used = {}; S[k].forEach(function (x) { used[x.pos] = 1; });
var el = p.el || [], pref = (p.pp && POS.indexOf(p.pp) >= 0 && el.indexOf(p.pp) >= 0) ? [p.pp] : [];
var cand = pref.concat(POS.filter(function (q) { return q === 'DH' || el.indexOf(q) >= 0; }));
for (var i = 0; i < cand.length; i++) if (!used[cand[i]]) return cand[i];
return cand[0] || 'DH';
}
function add(a, id) {
var p = P(id);
if (a === 'lr' || a === 'll') {
if (S[a].some(function (x) { return x.id === id; })) return;
if (S[a].length >= 9) { toast('That lineup already has 9. Remove someone first.'); return; }
S[a].push({ id: id, pos: firstOpen(a, p) }); var bk = a === 'lr' ? 'br' : 'bl'; S[bk] = S[bk].filter(function (x) { return x !== id; });
} else if (a === 'bb') {
['br', 'bl'].forEach(function (k) { var lk = k === 'br' ? 'lr' : 'll'; if (S[k].indexOf(id) < 0 && !S[lk].some(function (x) { return x.id === id; })) S[k].push(id); });
} else if (a === 'rot') {
if (S.rot.indexOf(id) >= 0) return; if (S.rot.length >= 5) { toast('The rotation is full (5). Remove a starter first.'); return; }
S.pen = S.pen.filter(function (x) { return x !== id; }); S.rot.push(id);
} else if (a === 'pen') { if (S.pen.indexOf(id) >= 0) return; S.rot = S.rot.filter(function (x) { return x !== id; }); S.pen.push(id); }
save(); render();
}
function autoFill() {
var md = model(), s26 = in26(), prev = JSON.stringify({ lr: S.lr, ll: S.ll, br: S.br, bl: S.bl, rot: S.rot, pen: S.pen });
var L40 = Object.keys(md.mine).map(P).filter(function (p) { return is40(p, md, s26); });
var pw = function (p) { var q = p.fg && p.fg.p; return q && q[2] >= 5 ? q[0] : -9; }, gs = function (p) { var q = p.fg && p.fg.p; return q ? q[3] || 0 : 0; };
var hw = function (p) { var b = p.fg && p.fg.b; return b && b[2] ? b[0] + b[2] / 1e4 : -9; };
var used = function () { return in26(); };
var pitchers = L40.filter(function (p) { return isP(p) && !p.tw; });
var nPit = function () { var u = {}; S.rot.concat(S.pen).forEach(function (id) { var p = P(id); if (p && !p.tw) u[id] = 1; }); return Object.keys(u).length; };
pitchers.slice().sort(function (a, b) { return (role(b) === 'SP') - (role(a) === 'SP') || gs(b) - gs(a) || pw(b) - pw(a); }).forEach(function (p) { if (S.rot.length < 5 && !used()[p.id]) S.rot.push(p.id); });
var hitters = L40.filter(function (p) { return canHit(p) && (!isP(p) || p.tw); }).sort(function (a, b) { return hw(b) - hw(a); });
var targetPit = 13;
['lr', 'll'].forEach(function (k) {
var wasEmpty = !S[k].length, have = {}; S[k].forEach(function (x) { have[x.pos] = 1; });
['C', 'SS', 'CF', '2B', '3B', 'RF', 'LF', '1B', 'DH'].forEach(function (q) {
if (have[q] || S[k].length >= 9) return;
var inL = {}; S[k].forEach(function (x) { inL[x.id] = 1; });
var ok = function (p) { return !inL[p.id] && (Object.keys(in26()).length < 26 || in26()[p.id]); };
var plays = function (p) { return String(posLab(p)).split('/').indexOf(q) >= 0; };
var cand = q === 'DH' ? hitters.filter(ok)[0] : (hitters.filter(function (p) { return ok(p) && plays(p); })[0] || hitters.filter(function (p) { return ok(p) && (p.el || []).indexOf(q) >= 0; })[0]);
if (cand) { S[k].push({ id: cand.id, pos: q }); have[q] = 1; var bk = k === 'lr' ? 'br' : 'bl'; S[bk] = S[bk].filter(function (x) { return x !== cand.id; }); }
});
if (wasEmpty) S[k].sort(function (a, b) { var A = P(a.id).fg, B = P(b.id).fg; return ((B && B.b && B.b[1]) || 0) - ((A && A.b && A.b[1]) || 0); });
});
var hitSlots = function () { return 26 - Math.max(targetPit, nPit()); };
var hitN = function () { var u = {}; S.lr.concat(S.ll).forEach(function (x) { u[x.id] = 1; }); S.br.concat(S.bl).forEach(function (id) { u[id] = 1; }); return Object.keys(u).length; };
var benchAdd = function (p) { ['br', 'bl'].forEach(function (k) { var lk = k === 'br' ? 'lr' : 'll'; if (S[k].indexOf(p.id) < 0 && !S[lk].some(function (x) { return x.id === p.id; })) S[k].push(p.id); }); };
var onC = function () { var n = 0, u = used(); hitters.forEach(function (p) { if (u[p.id] && (p.el || []).indexOf('C') >= 0) n++; }); return n; };
if (onC() < 2) { var c2 = hitters.filter(function (p) { return !used()[p.id] && (p.el || []).indexOf('C') >= 0; })[0]; if (c2 && hitN() < hitSlots()) benchAdd(c2); }
var stuckF = function (p) { return p.noOpt && (p.on40 || md.from[p.id] !== 'base') ? 1 : 0; };
hitters.slice().sort(function (a, b) { return stuckF(b) - stuckF(a); }).forEach(function (p) { if (hitN() < hitSlots() && !used()[p.id]) benchAdd(p); });
pitchers.slice().sort(function (a, b) { return stuckF(b) - stuckF(a) || pw(b) - pw(a); }).forEach(function (p) { if (Object.keys(used()).length < 26 && nPit() < targetPit && !used()[p.id]) S.pen.push(p.id); });
view.afPrev = prev; save(); render();
var n = Object.keys(in26()).length;
toast(n >= 26 ? 'Filled the open spots. Change anyone you want, or Undo auto-fill.' : 'Filled what we could (' + n + ' of 26). Add more players to finish.');
}
function openCard(id) {
var p = P(id), md = model(), box = $('#hg-modal'); if (!p) return;
var b = p.fg && p.fg.b, q = p.fg && p.fg.p, st = [];
if (b && b[2]) st.push(['Hitting', b[0] + ' fWAR', (b[1] == null ? '\u2014' : b[1]) + ' wRC+', b[2] + ' PA']);
if (q && q[2] >= 5) st.push(['Pitching', q[0] + ' fWAR', q[1] + ' WPA', q[2] + ' IP', (q[3] ? q[3] + ' GS' : q[4] + ' G')]);
var c = md.contract[p.id], yrs = [];
if (c) { for (var y = 2027; y <= c.start + c.sal.length - 1; y++) yrs.push([y, contractCash(c, y)]); }
else if (p.sal) p.sal.forEach(function (v, i) { if (v > 0) yrs.push([2027 + i, v]); });
var con = yrs.length ? yrs.map(function (r) { return '' + r[0] + ' ' + M(r[1]) + ''; }).join('') : '';
var notes = [];
if (c) notes.push('New deal in this plan: ' + dealText(c));
if (arbLive(p) && !c) notes.push('Arbitration (Arb ' + (p.arb.y || '') + '): projected ' + M(arbProj(p)) + ' for 2027');
if (p.optPending) notes.push(p.optPending.type.replace(/ OPTION/, '').toLowerCase().replace(/^./, function (m) { return m.toUpperCase(); }) + ' option for 2027' + (p.optPending.sal ? ': ' + M(p.optPending.sal) : '') + (p.optPending.buy ? ', ' + M(p.optPending.buy) + ' buyout' : ''));
if (!yrs.length && !c && !arbLive(p) && !p.optPending && (p.on40 || md.mine[p.id])) notes.push('Pre-arbitration or minor-league contract: counts at the league minimum while on the 26-man');
var rk = RK[p.t] && RK[p.t][p.id];
var facts = [posLab(p), (p.b || '?') + '/' + (p.th || '?'), age(p.dob) ? 'Age ' + age(p.dob) : '', p.t ? tname(p.t) : (p.faFrom ? '2026: ' + tname(p.faFrom) : ''), p.mls ? 'Service ' + p.mls : '', p.opt != null && p.on40 && !(p.mls && parseFloat(p.mls) >= 5) ? 'Options left ' + p.opt : '', rk ? 'No. ' + rk + ' prospect (' + (p.t === 'SEA' ? 'Baseball Things' : 'MLB Pipeline') + ')' : ''].filter(Boolean);
box.innerHTML = '
';
box.querySelectorAll('[data-close]').forEach(function (e) { e.onclick = function () { box.innerHTML = ''; }; });
pidx().then(function (ix) { var sl = ix && ix[p.id], a = box.querySelector('[data-plink]'); if (sl && a) { a.href = '/players/' + sl + '/'; a.style.display = ''; } });
}
var PIDX = null;
function pidx() { if (PIDX) return PIDX; PIDX = window.BT_PIDX_URL ? fetch(window.BT_PIDX_URL).then(function (r) { return r.json(); }).then(function (j) { return j.p || {}; }).catch(function () { return {}; }) : Promise.resolve({}); return PIDX; }
function offBoard(id) { id = +id; ['lr', 'll'].forEach(function (k) { S[k] = S[k].filter(function (x) { return x.id !== id; }); }); ['br', 'bl', 'rot', 'pen'].forEach(function (k) { S[k] = S[k].filter(function (x) { return x !== id; }); }); }
function cleanBoard() { var md = model(); ['lr', 'll'].forEach(function (k) { S[k] = S[k].filter(function (x) { return md.mine[x.id]; }); }); ['br', 'bl', 'rot', 'pen'].forEach(function (k) { S[k] = S[k].filter(function (x) { return md.mine[x]; }); }); }
var tT = null;
function toast(m) { var d = $('#hg-msg'); if (!d) { d = document.createElement('div'); d.id = 'hg-msg'; d.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:#1d3a63;color:#fff;padding:10px 16px;border-radius:10px;z-index:99;box-shadow:0 8px 20px rgba(0,0,0,.4);font-size:14px'; document.body.appendChild(d); } d.textContent = m; d.style.display = 'block'; clearTimeout(tT); tT = setTimeout(function () { d.style.display = 'none'; }, 2600); }
function makeTrade() {
var md = model(), sendSel = Object.keys(view.tsend).map(P), recvSel = Object.keys(view.trecv).map(P), cs = view.tcash;
var eff = tradeEffect(md, sendSel, recvSel, cs); if (eff.err) return;
S.moves.push({ k: 'trade', mid: S.seq++, tm: view.tp, send: sendSel.map(function (p) { return p.id; }), recv: recvSel.map(function (p) { return p.id; }), cash: cs.dir === 'none' || !eff.amt ? null : { dir: cs.dir, amt: eff.amt, split: eff.split } });
view.tsend = {}; view.trecv = {}; view.tcash = { dir: 'none', amt: '', split: ['', '', '', '', ''] };
cleanBoard(); save(); render(); toast('Trade made. It’s in Your moves if you want to undo it.');
}
function undoMove(arg) {
if (/^dfa:/.test(arg)) { var id = +arg.split(':')[1]; S.moves = S.moves.filter(function (m) { return !(m.k === 'dfa' && m.id === id); }); }
else { var mid = +arg; S.moves = S.moves.filter(function (m) { return m.mid !== mid; }); }
cleanBoard(); save(); render();
}
// ---------- events ----------
var DR = null;
function wire() {
var m = $('#hg-main');
function on(sel, fn) { m.querySelectorAll(sel).forEach(function (b) { b.onclick = function (e) { fn(b, e); }; }); }
on('[data-mode]', function (b) { view.mode = b.getAttribute('data-mode'); render(); });
on('[data-mode-go]', function (b) { view.mode = b.getAttribute('data-mode-go'); render(); });
on('[data-rtab]', function (b) { view.rtab = b.getAttribute('data-rtab'); render(); });
on('[data-fag]', function (b) { view.fag = b.getAttribute('data-fag'); render(); });
m.querySelectorAll('input[data-q]').forEach(function (q) { q.oninput = function () { var k = q.getAttribute('data-q'); view[k] = q.value; var pos = q.selectionStart; render(); var q2 = $('#hg-main input[data-q="' + k + '"]'); if (q2) { q2.focus(); q2.setSelectionRange(pos, pos); } }; });
on('[data-plan]', function (b) { openPlan(+b.getAttribute('data-plan')); });
m.querySelectorAll('.slot[data-dk]').forEach(function (el) {
var k = el.getAttribute('data-dk'), i = +el.getAttribute('data-di');
el.ondragstart = function (e) { DR = { k: k, i: i }; try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', k); } catch (x) { } el.classList.add('drag'); };
el.ondragend = function () { el.classList.remove('drag'); DR = null; };
el.ondragover = function (e) { if (DR && DR.k === k) { e.preventDefault(); el.classList.add('over'); } };
el.ondragleave = function () { el.classList.remove('over'); };
el.ondrop = function (e) { e.preventDefault(); el.classList.remove('over'); if (!DR || DR.k !== k || S.locked || roFree()) return; var L = S[k], it = L.splice(DR.i, 1)[0]; L.splice(Math.min(i, L.length), 0, it); DR = null; save(); render(); };
});
on('[data-fs]', function (b) { view.fsort = b.getAttribute('data-fs'); render(); });
on('[data-lock]', function () { if (S.locked) { toast(lockMsg()); return; } openLock(); });
var bro = m.querySelector('[data-rolock]'); if (bro) bro.onfocus = function () { bro.blur(); toast(lockMsg()); };
var grid = m.querySelector('.hg-grid.hg-ro');
if (grid) {
var block = function (e) { var t = e.target.closest ? e.target.closest('button,select,.slot,input,label') : null; if (!t || t.closest(VIEW_OK) || (t.matches && t.matches(VIEW_OK))) return; e.preventDefault(); e.stopPropagation(); if (e.type !== 'mousedown') toast(lockMsg()); };
grid.addEventListener('click', block, true);
grid.addEventListener('mousedown', function (e) { if (e.target.closest && e.target.closest('select') && !e.target.closest(VIEW_OK)) { e.preventDefault(); toast(lockMsg()); } }, true);
grid.addEventListener('dragstart', function (e) { e.preventDefault(); toast(lockMsg()); }, true);
}
on('[data-fl]', function (b) { view.falvl = b.getAttribute('data-fl'); render(); });
on('[data-msign]', function (b) { var id = +b.getAttribute('data-msign'); S.moves.push({ k: 'msign', mid: S.seq++, id: id }); save(); render(); toast('Signed to a minor league deal. He\u2019s in your Prospects list.'); });
on('[data-add40]', function (b) { var id = +b.getAttribute('data-add40'); S.moves.push({ k: 'add40', mid: S.seq++, id: id }); save(); render(); toast('Added to the 40-man. Undo it from the 40-man tab or Your moves.'); });
on('[data-r5only]', function () { view.r5only = !view.r5only; render(); });
on('[data-clear]', function () {
var box = $('#hg-modal');
box.innerHTML = '
Clear your ' + esc(D.teams[team].name) + ' plan?
This erases every move, lineup, rotation and bullpen spot' + (S.locked ? ', and the lock on \u201c' + esc(S.locked.name) + '\u201d' : '') + '. You start over from the real roster. This can\u2019t be undone.
';
box.querySelectorAll('[data-close]').forEach(function (e) { e.onclick = function () { box.innerHTML = ''; }; });
box.querySelector('[data-clrok]').onclick = function () {
if (!canClear()) return;
var done = function () { S = blank(); S._pid = null; try { localStorage.setItem(key(), JSON.stringify(S)); } catch (e) { } box.innerHTML = ''; render(); toast('Plan cleared. Start fresh.'); };
if (SRV.ready && S._pid) { var pid = S._pid; api('DELETE', '/api/plans/' + pid).then(function (res) { if (res.s === 200) { SRV.plans = SRV.plans.filter(function (p) { return p.id !== pid; }); done(); } else toast('Couldn\u2019t clear it. Try again.'); }); }
else done();
};
});
on('[data-ptab]', function (b) { view.ptab = b.getAttribute('data-ptab'); render(); });
on('[data-mp]', function (b) { view.mp = b.getAttribute('data-mp'); render(); window.scrollTo(0, Math.min(window.scrollY, ($('#hg-main') || document.body).offsetTop)); });
on('[data-autofill]', function () { autoFill(); });
on('[data-afundo]', function () { if (!view.afPrev) return; var o = JSON.parse(view.afPrev); Object.keys(o).forEach(function (k) { S[k] = o[k]; }); view.afPrev = null; save(); render(); toast('Auto-fill undone.'); });
on('[data-more]', function (b, e) { e.stopPropagation(); var w = b.parentNode, was = w.classList.contains('open'); m.querySelectorAll('.hg-mw.open').forEach(function (x) { x.classList.remove('open'); }); if (!was) w.classList.add('open'); });
m.querySelectorAll('[data-pc]').forEach(function (e) { e.onclick = function () { openCard(+e.getAttribute('data-pc')); }; e.onkeydown = function (ev) { if (ev.key === 'Enter') openCard(+e.getAttribute('data-pc')); }; });
var bud = m.querySelector('input[data-budget]'); if (bud) { bud.onchange = function () { var v = bud.value.trim(); S.budget = v ? num(v) : 0; save(); render(); }; bud.onkeydown = function (ev) { if (ev.key === 'Enter') bud.blur(); }; }
on('[data-tm2]', function (b) { pick(b.getAttribute('data-tm2')); window.scrollTo(0, 0); });
on('[data-unlock]', function () {
if (!canClear()) { toast(lockMsg()); return; }
if (!SRV.ready || !S._pid) { delete S.locked; save(); render(); return; }
api('POST', '/api/plans/' + S._pid + '/unlock').then(function (res) {
if (res.s === 200) { var i = SRV.plans.findIndex(function (p) { return p.id === res.j.plan.id; }); if (i >= 0) SRV.plans[i] = res.j.plan; delete S.locked; save(); render(); toast('Unlocked. Make your changes, then lock it in again.'); }
else toast(res.s === 403 ? lockMsg() : 'Couldn\u2019t unlock it. Try again.');
});
});
on('[data-snap]', function () { openSnap(); });
on('[data-plantog]', function () { PLAN_OPEN = !PLAN_OPEN; try { localStorage.setItem('hsgm_plan_open', PLAN_OPEN ? '1' : '0'); } catch (e) { } render(); });
on('[data-ts]', function (b) { var v = b.getAttribute('data-ts').split(':'); view['ts' + v[0]] = v[1]; render(); });
on('[data-pf]', function (b) { var v = b.getAttribute('data-pf'), wasP = /^(SP|RP)$/.test(view.pf), isP2 = /^(SP|RP)$/.test(v); view.hand = ''; view.pf = v; view.psub = ''; render(); });
on('[data-clrf]', function () { view.pf = 'all'; view.psub = ''; view.hand = ''; view.faq = ''; render(); });
on('[data-hd]', function (b) { view.hand = b.getAttribute('data-hd'); render(); });
on('[data-ps]', function (b) { view.psub = b.getAttribute('data-ps'); render(); });
on('[data-trade]', function (b) { var id = b.getAttribute('data-trade'); view.mode = 'trade'; view.tsend = {}; view.tsend[id] = 1; view.pf = 'all'; render(); toast('Pick a trade partner, then choose what you get back.'); });
on('[data-t]', function (b) { var id = +b.getAttribute('data-id'), v = b.getAttribute('data-t'); if (v) S.tender[id] = v; else delete S.tender[id]; if (v === 'nt') offBoard(id); save(); render(); });
on('[data-o]', function (b) { var id = +b.getAttribute('data-id'), v = b.getAttribute('data-o'); if (v) S.option[id] = v; else delete S.option[id]; if (v !== 'dec') delete S.tender[id]; if (v === 'dec' && optLeaves(P(id), id)) offBoard(id); save(); render(); });
on('[data-undo-t]', function (b) { delete S.tender[b.getAttribute('data-undo-t')]; save(); render(); });
on('[data-undo-o]', function (b) { var id = b.getAttribute('data-undo-o'); delete S.option[id]; delete S.tender[id]; save(); render(); });
on('[data-undo-m]', function (b) { undoMove(b.getAttribute('data-undo-m')); });
on('[data-dfa]', function (b) { var id = +b.getAttribute('data-dfa'); S.moves.push({ k: 'dfa', mid: S.seq++, id: id }); offBoard(id); save(); render(); toast('Designated for assignment. Undo it from Departures or Your moves.'); });
on('[data-ext]', function (b) { openContract(+b.getAttribute('data-ext'), 'ext'); });
on('[data-sign]', function (b) { openContract(+b.getAttribute('data-sign'), 'sign'); });
var tp = m.querySelector('select[data-tp]'); if (tp) tp.onchange = function () { view.tp = tp.value; view.trecv = {}; view.ts2 = '40'; view.tq2 = ''; render(); };
m.querySelectorAll('input[data-tr]').forEach(function (c) { c.onchange = function () { var o = c.getAttribute('data-tr') === 's' ? view.tsend : view.trecv, id = c.getAttribute('data-id'); if (c.checked) o[id] = 1; else delete o[id]; render(); }; });
var cd = m.querySelector('select[data-cdir]'); if (cd) cd.onchange = function () { view.tcash.dir = cd.value; render(); };
function keepFocus(sel, setter) { var e = m.querySelector(sel); if (e) e.oninput = function () { setter(e.value); var pos = e.selectionStart; render(); var e2 = $('#hg-main ' + sel); if (e2) { e2.focus(); e2.setSelectionRange(pos, pos); } }; }
keepFocus('input[data-camt]', function (v) { view.tcash.amt = v; });
YRS.forEach(function (y, i) { keepFocus('input[data-csp="' + i + '"]', function (v) { view.tcash.split[i] = v; }); });
on('[data-maketrade]', function () { makeTrade(); });
m.querySelectorAll('select[data-k]').forEach(function (s) { s.onchange = function () { if (S.locked || roFree()) { render(); return; } S[s.getAttribute('data-k')][+s.getAttribute('data-i')].pos = s.value; save(); render(); }; });
function mv(attr, d) { on('[' + attr + ']', function (b) { var k = b.getAttribute(attr), i = +b.getAttribute('data-i'), L = S[k], j = i + d; if (j < 0 || j >= L.length) return; var t = L[i]; L[i] = L[j]; L[j] = t; save(); render(); }); }
mv('data-up', -1); mv('data-dn', 1);
on('[data-rm]', function (b) { S[b.getAttribute('data-rm')].splice(+b.getAttribute('data-i'), 1); save(); render(); });
}
function boot() {
document.addEventListener('click', function (e) { if (!e.target.closest || !e.target.closest('.hg-mw')) document.querySelectorAll('#hsgm .hg-mw.open').forEach(function (x) { x.classList.remove('open'); }); else if (e.target.closest('.hg-menu button')) e.target.closest('.hg-mw').classList.remove('open'); });
window.addEventListener('beforeunload', function (e) { if (team && S && !S.locked && !roFree() && hasWork() && (!SRV.ready || SRV.state === 'saving' || SRV.state === 'error')) { e.preventDefault(); e.returnValue = ''; } });
try { var st2 = document.createElement('style'); st2.textContent = UXCSS; document.head.appendChild(st2); } catch (e) { }
try { var st = document.createElement('style'); st.textContent = '.hg-freebar{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;background:rgba(240,180,60,.1);border:1px solid rgba(240,180,60,.45);border-radius:12px;padding:10px 14px;margin:-6px 0 16px}.hg-freebar b{color:#f0b43c}.hg-freebar span{color:var(--mute);font-size:13.5px;flex:1 1 320px}.hg-freebar a.hg-go{text-decoration:none}.hg-ronote a{color:inherit;text-decoration:underline}'; document.head.appendChild(st); } catch (e) { }
var tz = document.getElementById('hsgm-teaser'); if (tz && window.HSGM_DATA_URL) tz.style.display = 'none';
var root = document.getElementById('hsgm'); if (!root || !window.HSGM_DATA_URL) return;
root.innerHTML = '