/* 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.
'; } function pick(tc) { team = tc; load(); if (SRV.ready) applyServer(); SRV.state = ''; $('#hsgm').querySelectorAll('.hg-strip button').forEach(function (b) { b.classList.toggle('on', b.getAttribute('data-tm') === tc); }); view.mode = 'roster'; view.rtab = '40'; view.q = ''; view.tp = ''; view.afPrev = null; view.mp = 'list'; view.tsend = {}; view.trecv = {}; render(); } // ---------- render ---------- function render() { var keep = view.mode + '|' + view.rtab + '|' + view.tp, wy = window.scrollY, sc = []; document.querySelectorAll('#hg-main .hg-list, #hg-main .hg-trl').forEach(function (e) { sc.push(e.scrollTop); }); render0(); if (keep === view._keep) { document.querySelectorAll('#hg-main .hg-list, #hg-main .hg-trl').forEach(function (e, i) { if (sc[i] != null) e.scrollTop = sc[i]; }); window.scrollTo(0, wy); } view._keep = keep; } function budgetCells(PY) { var b = S.budget || 0, RO = S.locked || roFree(); var inp = '
Your budget
'; if (!b) return inp; var d = b - PY.total; return inp + '
' + (d >= 0 ? 'Room left' : 'Over budget') + '' + M(Math.abs(d)) + '
'; } function render0() { var main = $('#hg-main'), T = D.teams[team], PY = payroll(), md = PY.md, C = counts(md), b = PY.br; function item(lbl, arr, show) { return (show || arr[0] || arr[1]) ? '
' + lbl + '' + M(arr[0]) + (arr[1] ? ' (' + (arr[1] > 0 ? '+' : '') + arr[1] + ')' : '') + '
' : ''; } var pay = '
' + esc(T.name) + ' committed 2027' + M(PY.total) + ' (' + PY.n + ')
' + '
Starting point' + M(PY.base) + ' (' + PY.n0 + ')
' + item('Arbitration', b.arb) + item('Options', b.opt) + item('Free agents', b.fa) + item('Trades', b.trade) + item('Extensions', b.ext) + item('Minimum salaries*', b.min) + budgetCells(PY) + '
26-man' + C.n26 + '/26
' + '
40-man' + C.forty + '/40
' + '
Pitchers' + C.pit + '/13
'; var nMoves = movesList(md).length; var modes = [['roster', 'Roster'], ['arb', 'Arbitration'], ['opt', 'Options'], ['fa', 'Free agents'], ['trade', 'Trades'], ['moves', 'Your moves (' + nMoves + ')']]; var modeBar = '
' + modes.map(function (m) { return ''; }).join('') + (canClear() ? '' : '') + (roFree() ? '' : '') + '' + (S.locked || roFree() ? '' : '' + savedTxt() + '') + '
'; var mtog = '
'; var left = view.mode === 'roster' ? rosterHTML(md) : view.mode === 'arb' ? arbHTML(md) : view.mode === 'opt' ? optHTML(md) : view.mode === 'fa' ? faHTML(md) : view.mode === 'trade' ? tradeHTML(md) : movesHTML(md); var lockBar = S.locked ? '
\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
' : ''; var RO = S.locked || roFree(); main.innerHTML = pay + modeBar + lockBar + freeBar + mtog + '
' + (S.locked ? '
\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 '
' + [['all', 'All'], ['SP', 'Starters'], ['RP', 'Relievers'], ['C', 'Catchers'], ['IF', 'Infielders'], ['OF', 'Outfielders'], ['DH', 'DH']].map(function (x) { return ''; }).join('') + '
' + (SUB ? '
' + SUB.map(function (x) { return ''; }).join('') + '10+ games there in 2026
' : '') + handChips(); } function handChips() { if (view.pf === 'all') return ''; var pit = view.pf === 'SP' || view.pf === 'RP', o = pit ? [['', 'All'], ['L', 'LHP'], ['R', 'RHP']] : [['', 'All'], ['L', 'Bats L'], ['R', 'Bats R'], ['S', 'Switch']]; return '
' + o.map(function (x) { return ''; }).join('') + '
'; } function rosterHTML(md) { var s26 = in26(), mine = Object.keys(md.mine).map(P); var tabs = '

Roster

' + posChips(); var html = ''; if (view.rtab === '40') { var L = pfilt(search(mine.filter(function (p) { return is40(p, md, s26); }), view.q)); var hit = L.filter(function (p) { return !isP(p) || p.tw; }).sort(byLast), pit = L.filter(function (p) { return isP(p) && !p.tw; }).sort(byLast); var btn = function (p) { return moreMenu('' + (md.add40[p.id] ? '' : '')); }; html = (hit.length ? '
Position players (' + hit.length + ')
' + hit.map(function (p) { return plRow(p, md, s26, btn(p)); }).join('') : '') + (pit.length ? '
Pitchers (' + pit.length + ')
' + 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.
' + gone.map(function (p) { var why = p.pendingFA ? 'Free agent' : S.tender[p.id] === 'nt' ? 'Non-tendered' : optLeaves(p, p.id) ? 'Option declined' : md.sent[p.id] ? 'Traded to the ' + tname(md.sent[p.id]) : 'Designated for assignment'; var undo = p.pendingFA ? '' : S.tender[p.id] === 'nt' ? '' : optLeaves(p, p.id) ? '' : md.dfa[p.id] ? '' : ''; return '
' + nmH(p) + ' ' + esc(why) + '
' + undo + '
' + metaOf(p, null).map(esc).join('·') + '
'; }).join(''); } return tabs + '
' + (html || '
Nobody here.
') + '
'; } function arbHTML(md) { var s26 = in26(), all = Object.keys(D.players).map(P).filter(function (p) { return p.arb && (p.t === team || md.mine[p.id]) && !p.pendingFA && !md.sent[p.id] && (!p.optPending || ctlDec(p)); }); all.sort(byLast); var rows = all.map(function (p) { if (!arbPending(p)) return '
' + nmH(p) + ' Settled
Agreed: ' + M(p.arb.a) + '·Real result; can’t be changed
'; if (md.contract[p.id]) return '
' + nmH(p) + ' New deal
Signed to a multi-year deal in this plan
'; var v = S.tender[p.id] || ''; return '
' + nmH(p) + ' Arb ' + (p.arb.y || '') + '
' + [p.pp, 'MLS ' + (p.mls || ''), '2026 salary: see tracker', 'Projected 2027: ' + M(arbProj(p))].map(esc).join('·') + '
'; }).join(''); return '

Arbitration

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 '
' + nmH(p) + ' ' + esc(p.optPending.type.toLowerCase()) + '
2027 option ' + M(p.optPending.sal) + '·Buyout ' + M(p.optPending.buy) + '·' + (p.optPending.ctl ? 'If declined: arbitration-eligible' + (p.arb && p.arb.p ? ' (proj. ' + M(p.arb.p) + ')' : '') : 'If declined: free agent') + '' + (p.optPending.note ? '·' + esc(p.optPending.note) + '' : '') + '
'; }).join(''); return '

Options

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 = '
Sort:' + opts.map(function (o) { return ''; }).join('') + '
'; var signed = S.moves.filter(function (m) { return m.k === 'sign' || m.k === 'msign'; }); var top = signed.length ? '
Signed in this plan
' + signed.map(function (m) { var p = P(m.id); return '
' + nmH(p) + ' ' + esc(m.k === 'msign' ? 'Minor-league deal' : dealText(m.c)) + '
'; }).join('') : ''; var rows = L.map(function (p) { return '
' + nmH(p) + '
' + [posLab(p), (p.b || '?') + '/' + (p.th || '?'), age(p.dob) ? 'Age ' + age(p.dob) : '', p.faFrom ? '2026: ' + tname(p.faFrom) : '', p.mi && p.faWhy ? p.faWhy : ''].filter(Boolean).map(esc).join('·') + '
' + (line(p) ? '
' + esc(line(p)) + '
' : '') + '
'; }).join(''); return '

Free agents

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 += '
You send (' + sendSel.length + ')
' + tsw(1) + '
' + mine.slice(0, 400).map(function (p) { return row(p, 's'); }).join('') + '
You get from the ' + esc(tname(view.tp)) + ' (' + recvSel.length + ')
' + tsw(2) + '
' + theirs.slice(0, 400).map(function (p) { return row(p, 'r'); }).join('') + '
' + '
Cash
' + (cs.dir !== 'none' ? ' Split by year ($M): ' + YRS.map(function (y, i) { return ''; }).join('') : '') + '
' + '
Your committed 2027: ' + M(eff.me) + '
' + esc(tname(view.tp)) + ' committed 2027: ' + M(basePay(view.tp)) + ' → ' + M(eff.them) + ' (for reference only)
' + (eff.err ? '
' + esc(eff.err) + '
' : '') + ''; } return html; } function owed(p) { var t = 0; (p.sal || []).forEach(function (v) { if (v > 0) t += v; }); return t; } function tradeEffect(md, sendSel, recvSel, cs) { var err = '', amt = cs.dir === 'none' ? 0 : num(cs.amt), split = cs.split.map(function (v) { return v === '' ? 0 : num(v); }); if (cs.dir !== 'none' && amt) { var tot = split.reduce(function (a, b) { return a + b; }, 0); var cashOnly = (cs.dir === 'send' && !sendSel.length) || (cs.dir === 'recv' && !recvSel.length); if (cashOnly) { if (amt > 10e6) err = 'Cash on its own is capped at $10M.'; split = [amt, 0, 0, 0, 0]; } else { if (Math.abs(tot - amt) > 1000) err = 'The yearly split has to add up to the total (' + M(amt) + ').'; var cover = (cs.dir === 'send' ? sendSel : recvSel).reduce(function (a, p) { return a + owed(p); }, 0); if (amt > cover) err = 'Cash can’t be more than what the players in the deal are still owed (' + M(cover) + ').'; } } var me = payroll().total, them = basePay(view.tp); sendSel.forEach(function (p) { var v = money27(p, md, false); me -= v; them += v; }); recvSel.forEach(function (p) { var v = money27(p, md, false); me += v; them -= v; }); var c27 = cs.dir === 'none' ? 0 : split[0]; if (cs.dir === 'send') { me += c27; them -= c27; } else if (cs.dir === 'recv') { me -= c27; them += c27; } return { me: me, them: them, err: err, split: split, amt: amt }; } function tradeText(m) { var s = m.send.map(function (id) { return P(id).n; }).join(', ') || 'nothing', r = m.recv.map(function (id) { return P(id).n; }).join(', ') || 'nothing'; var c = m.cash && m.cash.amt ? (m.cash.dir === 'send' ? '; you send ' : '; you get ') + M(m.cash.amt) + ' cash' : ''; return 'You send ' + s + '; you get ' + r + c; } function fwar(p) { var b = p.fg && p.fg.b, q = p.fg && p.fg.p; return Math.round(((b ? b[0] || 0 : 0) + (q && q[2] >= 5 ? q[0] || 0 : 0)) * 10) / 10; } function movesGrouped(md) { var W = function (w) { return w + ' fWAR'; }, by = function (a, b) { return b.w - a.w; }, lines = function (L) { return L.sort(by).map(function (x) { return x.t; }); }; var tn = [], nt = [], up = [], dec = [], fa = [], tr = [], ex = [], dfa = [], ad = [], mi = []; Object.keys(S.tender).forEach(function (id) { var p = P(id); if (!p) return; var w = fwar(p); (S.tender[id] === 't' ? tn : nt).push({ w: w, t: p.n + ' · proj. ' + M(arbProj(p)) + ' · ' + W(w) }); }); Object.keys(S.option).forEach(function (id) { var p = P(id); if (!p || !p.optPending) return; var w = fwar(p); if (S.option[id] === 'up') up.push({ w: w, t: p.n + ' · ' + M(p.optPending.sal) + ' · ' + W(w) }); else dec.push({ w: w, t: p.n + ' · buyout ' + M(p.optPending.buy || 0) + ' · ' + W(w) }); }); S.moves.forEach(function (m) { if (m.k === 'sign') { var p = P(m.id), w = fwar(p); fa.push({ w: w, t: p.n + ' · ' + dealText(m.c) + ' · ' + W(w) }); } else if (m.k === 'ext') { var p2 = P(m.id), w2 = fwar(p2); ex.push({ w: w2, t: p2.n + ' · ' + dealText(m.c) + ' · ' + W(w2) }); } else if (m.k === 'msign') { var p6 = P(m.id), w6 = fwar(p6); mi.push({ w: w6, t: p6.n + ' \u00b7 ' + posLab(p6) + (p6.fg ? ' \u00b7 ' + W(w6) : '') }); } else if (m.k === 'add40') { var p5 = P(m.id); if (md.add40[m.id]) ad.push({ w: -(RK[p5.t] && RK[p5.t][p5.id] || 99), t: p5.n + ' \u00b7 ' + posLab(p5) + (p5.r5 === 1 ? ' \u00b7 protected from Rule 5' : '') }); } else if (m.k === 'dfa') { var p3 = P(m.id), w3 = fwar(p3); dfa.push({ w: w3, t: p3.n + ' · ' + W(w3) }); } else if (m.k === 'trade') { var got = m.recv.map(P), gave = m.send.map(P), w4 = Math.round(got.reduce(function (a, p) { return a + fwar(p); }, 0) * 10) / 10; var cs = m.cash && m.cash.amt ? ' + ' + M(m.cash.amt) + ' cash' : ''; tr.push({ w: w4, t: 'From the ' + tname(m.tm) + ': ' + (got.map(function (p) { return p.n; }).join(', ') || 'nothing') + (m.cash && m.cash.dir === 'recv' ? cs : '') + ' for ' + (gave.map(function (p) { return p.n; }).join(', ') || 'nothing') + (m.cash && m.cash.dir === 'send' ? cs : '') + ' · ' + W(w4) }); } }); function blk(title, subs) { subs = subs.filter(function (x) { return x[1].length; }); return subs.length ? { title: title, subs: subs } : null; } return { arb: blk('ARBITRATION', [['TENDERED', lines(tn)], ['NON-TENDERED', lines(nt)]]), opt: blk('OPTIONS', [['PICKED UP', lines(up)], ['DECLINED', lines(dec)]]), fa: blk('FREE AGENTS', [[null, lines(fa)]]), ext: blk('EXTENSIONS', [[null, lines(ex)]]), trade: blk('TRADES', [[null, lines(tr)]]), add: blk('ADDED TO 40-MAN', [[null, lines(ad)]]), mi: blk('MINOR LEAGUE DEALS', [[null, lines(mi)]]), dfa: blk('DFA', [[null, lines(dfa)]]) }; } function movesList(md) { var L = []; Object.keys(S.tender).forEach(function (id) { var p = P(id); if (p) L.push({ t: (S.tender[id] === 't' ? 'Tendered ' : 'Non-tendered ') + p.n + (S.tender[id] === 't' ? ' (proj. ' + M(arbProj(p)) + ')' : ''), u: 'data-undo-t="' + id + '"' }); }); Object.keys(S.option).forEach(function (id) { var p = P(id); if (p) L.push({ t: (S.option[id] === 'up' ? 'Picked up ' : 'Declined ') + p.n + '’s ' + p.optPending.type.toLowerCase(), u: 'data-undo-o="' + id + '"' }); }); S.moves.forEach(function (m) { var p = m.id ? P(m.id) : null; if (m.k === 'add40') { L.push({ t: 'Added ' + p.n + ' to the 40-man', u: 'data-undo-m="' + m.mid + '"' }); return; } if (m.k === 'msign') { L.push({ t: 'Signed ' + p.n + ' to a minor league deal', u: 'data-undo-m="' + m.mid + '"' }); return; } var t = m.k === 'sign' ? 'Signed ' + p.n + ': ' + dealText(m.c) : m.k === 'ext' ? (m.c.kind === 'ext' ? 'Extended ' : 'Multi-year deal for ') + p.n + ': ' + dealText(m.c) : m.k === 'dfa' ? 'Designated ' + p.n + ' for assignment' : 'Trade with the ' + tname(m.tm) + ': ' + tradeText(m); L.push({ t: t, u: 'data-undo-m="' + m.mid + '"' }); }); return L; } function movesHTML(md) { var L = movesList(md); return '

Your moves

Every decision in this plan. Undo takes it back.
' + (L.map(function (x) { return '
' + esc(x.t) + '
'; }).join('') || '
No moves yet.
') + '
'; } function posOpts(p, cur) { var el = p.el || []; return POS.map(function (q) { var ok = q === 'DH' || el.indexOf(q) >= 0; return ok ? '' : ''; }).join(''); } function lineupHTML(k, title) { var L = S[k], rows = '', cnt = {}; L.forEach(function (x) { cnt[x.pos] = (cnt[x.pos] || 0) + 1; }); var missQ = POS.filter(function (q) { return !cnt[q]; }); for (var i = 0; i < 9; i++) { var x = L[i]; if (!x) { rows += '
' + (i + 1) + '' + (missQ.length ? 'Need ' + missQ.shift() : 'Empty') + '
'; continue; } var p = P(x.id); rows += '
' + (i + 1) + '' + esc(p.n) + '' + (p.b || '') + '
'; } return '

' + title + '

' + rows + '
'; } function listHTML(k, title, max) { var L = S[k], rows = ''; L.forEach(function (id, i) { var p = P(id); rows += '
' + (i + 1) + '' + esc(p.n) + '' + esc(p.pp || '') + (p.th && k !== 'br' && k !== 'bl' ? ' ' + p.th + 'HP' : '') + '
'; }); var fill = max ? max - L.length : (L.length ? 0 : 1); for (var j = 0; j < fill; j++) rows += '
' + (L.length + j + 1) + '' + (k === 'rot' ? 'Need a starter' : k === 'pen' ? 'Add relievers' : 'Add bench players') + '
'; return '

' + title + '

' + rows + '
'; } function needList(md, C) { var o = [], d26 = 26 - C.n26; if (d26 > 0) o.push(d26 + ' more for the 26-man'); else if (d26 < 0) o.push((-d26) + ' too many on the 26-man'); [['lr', 'vs. RHP'], ['ll', 'vs. LHP']].forEach(function (a) { var have = {}; S[a[0]].forEach(function (x) { have[x.pos] = 1; }); var miss = POS.filter(function (q) { return !have[q]; }); if (miss.length) o.push(miss.join(', ') + ' ' + a[1]); }); if (S.rot.length < 5) o.push((5 - S.rot.length) + ' SP'); if (C.pit > 13) o.push((C.pit - 13) + ' too many pitchers'); if (C.forty > 40) o.push('clear ' + (C.forty - 40) + ' 40-man spot' + (C.forty - 41 ? 's' : '')); 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]; }).length; if (stuck) o.push(stuck + ' out-of-options player' + (stuck > 1 ? 's' : '') + ' to place'); var arbU = mineL.filter(function (p) { return arbLive(p) && !S.tender[p.id] && !md.contract[p.id]; }).length; if (arbU) o.push(arbU + ' arbitration decision' + (arbU > 1 ? 's' : '')); var optU = mineL.filter(function (p) { return p.optPending && !S.option[p.id]; }).length; if (optU) o.push(optU + ' option decision' + (optU > 1 ? 's' : '')); return o; } function boardHTML(md, C) { var t = view.ptab || 'r', nb = function (L) { return L.length; }; var need = needList(md, C), RO = S.locked || roFree(); var head = need.length ? '
Still need' + need.map(function (x) { return '' + esc(x) + ''; }).join('') + '
' : '
\u2713 Ready to lock it in
'; var af = (!RO && C.n26 < 26) ? '
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.
'; var html = '

' + esc(title) + '

' + info + '
' + '
By year ($M)
' + yrs.map(function (y, i) { return ''; }).join('') + (yrs.length > 1 ? ' ' : '') + '
' + '
Assigned: ' + M(b.alloc) + ' of ' + (num(st.total || '') ? M(num(st.total)) : '\u2014') + '. The total includes any signing bonus and option buyout.
' + '
Signing bonus ($M, optional)
' + (st.bonus ? 'Paid out: ' + byears.map(function (y, i) { return ''; }).join('') : '') + '
' + '
Option for ' + (b.last + 1) + ' (optional)
' + (st.opt ? ' ' : '') + '
' + (yrs.length > 1 ? '
Opt-outs (optional)
' + yrs.slice(0, -1).map(function (y) { return ''; }).join('') + '
' : '') + '
' + (b.label ? '' + (b.label === 'ext' ? 'Extension' : 'Multi-year deal') + ' · ' : '') + yrs.length + ' year' + (yrs.length > 1 ? 's' : '') + ' (' + yrs[0] + (yrs.length > 1 ? '–' + b.last : '') + ') · total ' + M(tot) + ' · 2027 cash ' + M(contractCash(b.c, 2027)) + '
' + (b.errs.length ? '
' + b.errs.map(esc).join('
') + '
' : '') + '
'; box.innerHTML = html; box.querySelectorAll('[data-close]').forEach(function (e) { e.onclick = function () { view.ct = null; drawContract(); }; }); box.querySelectorAll('[data-ct]').forEach(function (e) { var k = e.getAttribute('data-ct'); var h = function () { if (k === 'n') { st.n = +e.value; st.sal = st.sal.slice(0, st.n); if (!st.touched) ctSpread(st); } else if (k === 'total') { st.total = e.value; if (!st.touched || st.n === 1) ctSpread(st); } else if (k === 'even') { st.touched = false; ctSpread(st); } else if (k === 'sal') { st.sal[+e.getAttribute('data-i')] = e.value; st.touched = true; } else if (k === 'bonus') { st.bonus = e.value; if (!st.touched) ctSpread(st); } else if (k === 'bs') st.bsplit[+e.getAttribute('data-i')] = e.value; else if (k === 'opt') st.opt = e.value; else if (k === 'osal') st.osal = e.value; else if (k === 'obuy') { st.obuy = e.value; if (!st.touched) ctSpread(st); } else if (k === 'out') st.outs[e.getAttribute('data-y')] = e.checked; var foc = document.activeElement, fk = foc && foc.getAttribute && foc.getAttribute('data-ct'), fi = foc && foc.getAttribute && foc.getAttribute('data-i'), pos = foc && foc.selectionStart; drawContract(); if (fk && fk !== 'n' && fk !== 'opt' && fk !== 'out' && fk !== 'even') { var sel = '#hg-modal [data-ct="' + fk + '"]' + (fi != null ? '[data-i="' + fi + '"]' : ''); var n2 = $(sel); if (n2) { n2.focus(); try { n2.setSelectionRange(pos, pos); } catch (er) { } } } }; if (e.tagName === 'SELECT' || e.type === 'checkbox') e.onchange = h; else if (e.tagName === 'BUTTON') e.onclick = h; else e.oninput = h; }); var ok = box.querySelector('[data-ctok]'); if (ok) ok.onclick = function () { var bb = ctBuild(st); if (bb.errs.length) return; S.moves = S.moves.filter(function (m) { return !((m.k === 'sign' || m.k === 'ext') && m.id === st.id); }); if (st.kind === 'sign') S.moves.push({ k: 'sign', mid: S.seq++, id: st.id, c: bb.c }); else { S.moves.push({ k: 'ext', mid: S.seq++, id: st.id, c: bb.c }); if (S.tender[st.id]) delete S.tender[st.id]; } view.ct = null; drawContract(); save(); render(); }; } // ---------- add to plan ---------- function where(id) { var w = { r: '', l: '', pit: '', pos: '' }; S.lr.forEach(function (x) { if (x.id === id) { w.r = 'lu'; w.pos = x.pos; } }); S.ll.forEach(function (x) { if (x.id === id) { w.l = 'lu'; if (!w.pos) w.pos = x.pos; } }); if (S.br.indexOf(id) >= 0) w.r = 'bn'; if (S.bl.indexOf(id) >= 0) w.l = 'bn'; if (S.rot.indexOf(id) >= 0) w.pit = 'rot'; if (S.pen.indexOf(id) >= 0) w.pit = 'pen'; return w; } function openPlan(id) { var p = P(id), w = where(id); view.pl = { id: id, r: w.r, l: w.l, pit: w.pit, pos: w.pos || firstOpen('lr', p), err: '' }; drawPlan(); } function drawPlan() { var st = view.pl, box = $('#hg-modal'); if (!st) { box.innerHTML = ''; return; } var p = P(st.id), hit = canHit(p), pit = canPitch(p); function radio(g, val, lbl) { return ''; } var h = '

Where does ' + esc(p.n) + ' fit?

' + metaOf(p, model()).map(esc).join(' · ') + '
'; if (hit) { h += '
'; h += '
Vs. right-handed pitchers
' + radio('r', 'lu', 'Lineup') + radio('r', 'bn', 'Bench') + radio('r', '', 'Not on it') + '
'; h += '
Vs. left-handed pitchers
' + radio('l', 'lu', 'Lineup') + radio('l', 'bn', 'Bench') + radio('l', '', 'Not on it') + '
'; if (st.r === 'lu' || st.l === 'lu') h += '
Position in the lineup
You can change it per lineup on the board.
'; } if (pit) h += '
Pitching staff
' + radio('pit', 'rot', 'Starting rotation') + radio('pit', 'pen', 'Bullpen') + radio('pit', '', 'Not on it') + '
'; if (st.err) h += '
' + esc(st.err) + '
'; h += '
'; box.innerHTML = h; box.querySelectorAll('[data-close]').forEach(function (e) { e.onclick = function () { view.pl = null; drawPlan(); }; }); box.querySelectorAll('[data-pg]').forEach(function (e) { e.onchange = function () { st[e.getAttribute('data-pg')] = e.value; st.err = ''; drawPlan(); }; }); box.querySelectorAll('[data-pq]').forEach(function (e) { e.onclick = function () { var q = e.getAttribute('data-pq'); st.r = q === 'lu' || q === 'plat' ? 'lu' : 'bn'; st.l = q === 'lu' || q === 'platl' ? 'lu' : 'bn'; st.err = ''; drawPlan(); }; }); var ps = box.querySelector('[data-ppos]'); if (ps) ps.onchange = function () { st.pos = ps.value; }; box.querySelector('[data-plok]').onclick = function () { var id = st.id, w = where(id), err = ''; if (st.r === 'lu' && w.r !== 'lu' && S.lr.length >= 9) err = 'The lineup vs. RHP already has 9. Take someone out of it first.'; if (st.l === 'lu' && w.l !== 'lu' && S.ll.length >= 9) err = 'The lineup vs. LHP already has 9. Take someone out of it first.'; if (st.pit === 'rot' && w.pit !== 'rot' && S.rot.length >= 5) err = 'The rotation already has 5. Take a starter out first.'; if (err) { st.err = err; drawPlan(); return; } function setLU(k, on) { var i = -1; S[k].forEach(function (x, j) { if (x.id === id) i = j; }); if (on) { if (i < 0) S[k].push({ id: id, pos: st.pos }); else if (w.pos !== st.pos) S[k][i].pos = st.pos; } else if (i >= 0) S[k].splice(i, 1); } function setL(k, on) { var i = S[k].indexOf(id); if (on && i < 0) S[k].push(id); if (!on && i >= 0) S[k].splice(i, 1); } setLU('lr', st.r === 'lu'); setLU('ll', st.l === 'lu'); setL('br', st.r === 'bn'); setL('bl', st.l === 'bn'); setL('rot', st.pit === 'rot'); setL('pen', st.pit === 'pen'); view.pl = null; drawPlan(); save(); render(); }; } // ---------- lock it in ---------- function openLock() { var md = model(), C = checks(md), ok = C.every(function (c) { return c[0]; }); var st = view.lk = view.lk || { name: '' }; var h = '

Lock in your plan

' + (ok ? '
Name your plan
' + (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 = '

' + esc(p.n) + '

' + facts.map(esc).join('\u00b7') + '
' + tags(p, md) + '
' + (st.length ? '
2026 MLB
' + st.map(function (r) { return '
' + r[0] + '' + r.slice(1).map(function (x) { return '' + esc(x) + ''; }).join('') + '
'; }).join('') : '') + ((con || notes.length) ? '
Contract
' + (con ? '
' + con + '
' : '') + notes.map(function (n) { return '
' + esc(n) + '
'; }).join('') : '') + '
'; 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 = '
Loading every 40-man roster and farm system…
'; fetch(window.HSGM_DATA_URL).then(function (r) { return r.json(); }).then(function (d) { D = d; window.__HSGM_D = d; buildRanks(); loadSEA(); shell(); srvInit(); fetch('/members/api/member/', { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (m) { MEM = m; if (team) render(); }).catch(function () { }); }).catch(function () { root.innerHTML = '
Couldn’t load the rosters. Refresh to try again.
'; }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();