Know every situation. Have a plan for every practice.
The complete situations guides, quiz games your players actually want to play, 150 ready-made practice plans and the drills to run them. All in one app on your phone at the field. Pay once and it’s yours to keep.
From the makers of the Defensive Situations Guide, trusted by 25,000+ coaches and parents.
50 full practices, each written in 60-, 90-, and 120-minute versions. Pick one and walk onto the field with a complete plan in hand.
344 drills — new ones added daily
The Drill Library
Every position, every skill level — pitching, hitting, infield, outfield, catching, base running, and conditioning, organized and searchable. New drills are added every day. Each bundle opens a different share of the library (see Pricing).
🔥7 new drills added in the last 7 days
Practice planner · free for 3 months
Weakness-Based Practice Plans
Pick the issue your team has, and the planner builds a full practice around fixing it — diagnosed down to the sub-cause, not just the general category.
Practice planner · free for 3 months
Custom Practice Plan Builder
Build your own practice station by station — pick a category, then the exact drill, for warmups, stations, and conditioning.
Trusted Nationwide
25,000+ coaches and parents nationwide trust Marietta Baseball Academy.
Coaches and parents across the country already use our guides to teach the game. The Toolkit puts all four of them, every drill, and a practice planner in one place.
4
Proven guides, complete
The same Defensive Situations, Base Running, Hitting, and Pitch Selection guides — every situation, inside the app.
344
Drills and growing
A drill library that keeps getting bigger, matched to the specific problems your team is having.
150
Ready-made practice plans
50 full practices, each in 60, 90 and 120-minute versions — pick one and run it tonight.
Pricing
The Digital Coach’s Bundle. Pay once, keep it.
Pick the bundle your team needs. Everything opens in the MBA Toolkit app on your phone, and every bundle comes with 3 months of the practice planner free.
Beginner
$87 one-time
The defense and base running essentials
Defensive Situations guide + Animated Defense
Base Running guide + Read & Run game
150 ready-made practice plans: 50 practices, each in 60, 90 and 120-minute versions
Every bundle includes 3 months of the practice planner, free
Fix a problem (pick what your team is struggling with and get the drills that fix the cause), Build your own practices, saved plans and 15 Team Passes for your players. After 3 months, keep the planner for $9.99/month, cancel anytime. If you don’t, your guides, games, ready-made plans and drills stay yours.
14-day money-back guarantee on Advanced and Travel. If the bundle isn’t right for your team, email support@mbaseballacademy.com within 14 days of buying for a full refund. The Beginner bundle is final sale.
Already own a bundle? Log in and upgrade any time. You pay only the difference.
Play A Preview
Try it for yourself.
READ & RUN FREE TRIAL
A BASE RUNNING SITUATIONS GUIDE GAME · MARIETTA BASEBALL ACADEMY
BASES 0 STREAK 0 PLAY 0/3
YOU'RE ON BASE. MAKE THE READ.
Every play is a real situation from the Base Running Situations Guide. Pick the runner's read and watch it play out. Read it right and you take the base. Read it wrong and you run into an out.
FREE TRIAL · 3 PLAYS
Keys: 1 · 2 · 3 to pick, Enter for the next play.
THAT'S THE FREE TRIAL
BASE BANDIT
Ready for the rest of the bases? Subscribe to the Marietta Baseball Academy Toolkit and play all 46 situations.
All 46 base running situations, 10 new plays every game
Quiz games for all four situations guides
327+ drills, pre-built practice plans and the practice planners
A PITCH SELECTION GUIDE GAME · MARIETTA BASEBALL ACADEMY
A · WINNING THE COUNT
PITCH 1 OF 10
Leadoff hitter, first pitch of the game
WHAT'S THE CALL?
SIT -- COUNT - OUTS -
K'S 0 STREAK 0 PITCH 0/0
or press Enter
YOU'RE ON THE MOUND. MAKE THE CALL.
Every pitch is a real situation from the Pitch Selection Guide. Pick the right pitch and location and he swings through it. Pick wrong and he makes you pay.
AGE GROUP
FREE TRIAL · 3 PITCHES
Keys: 1 · 2 · 3 to pick, Enter for the next pitch.
TRIAL COMPLETE
ACE
That was 3 of 40 game situations. Subscribe to get every situation, a fresh 10-pitch game every time, and both the 11–13U and 14U+ games.
Most practice plans ignore what's actually going wrong.
Common weakness
Overthrows to first base
Generic infield drills don't target arm accuracy specifically — so the same throw sails every game.
Common weakness
Missed ground balls
Without diagnosing footwork vs. glove work vs. reads, practice time gets spent on the wrong fix.
Common weakness
Can't find the strike zone
A pitcher who's wild needs a different drill than one who's just missing low — the app knows the difference.
Try It
Pick a weakness. Get the drill built to fix it.
This is exactly how the Weakness-Based planner works inside the app:
1
Pick a category
Hitting, fielding, outfield, pitching, catching, or base running.
→
2
Pick the problem
The general issue your team is actually having — like arm accuracy or reads.
→
3
Identify the weakness
The app narrows it down to the exact sub-cause, out of 400+ it can pinpoint.
→
4
Get suggested drills
Matched specifically to that sub-cause, not the general category.
→
5
Add to your plan
Drop the suggested drills straight into a practice plan, ready to run.
Inside the app
Try it below — the same three dropdowns as the app, with real drills from the library.
Start by picking a category.
A sample of the taxonomy and drills, shown for demonstration — the full app covers every weakness and matches against 344 drills across 400+ specific sub-causes.
Share With Your Team
Put the games and the situations in your players' hands.
The practice planner includes 15 Team Passes for your players and assistant coaches (free for your first 3 months). They get the quiz games plus up to 12 situations you pick, on their own phones, so the learning keeps going between practices.
Send one link
Drop it in your team chat or let parents scan the QR code at practice. Each phone, tablet or computer that opens it uses one pass.
Pick what they study
Choose up to 12 situations from the guides you own. Swap them out as your team learns them.
They play and learn
Call the Pitch, Read & Run, Know Your Job, and the animated defense for the plays you shared.
No player accounts, names or emails, and no ads or tracking on the team pages. You can turn off any pass from the app.
Your viewYour players' view
Questions
Frequently asked questions.
Is this a subscription?
No. The Digital Coach’s Bundle is a one-time purchase, and the guides, games, ready-made plans and drills in your bundle are yours to keep. The only optional subscription is the practice planner, at $9.99/month after your free 3 months.
What’s the practice planner, and what happens after 3 months?
The planner is Fix a problem, Build your own, saving your practice plans and 15 Team Passes. Every bundle gets it free for 3 months, and we’ll email you before the trial ends. Keep it for $9.99/month, or let it end: your saved plans are kept (locked) and come back if you turn it on later. Everything else in your bundle stays open.
Can I upgrade to a bigger bundle later?
Yes. Choose Advanced or Travel any time and you pay only the difference from the bundle you already own.
Is there a money-back guarantee?
Yes, on the Advanced and Travel bundles. If either isn’t right for your team, email support@mbaseballacademy.com within 14 days of buying and we’ll refund you in full. The Beginner bundle is final sale. The $9.99/month planner, if you keep it after your free 3 months, isn’t refunded for partial months.
What happened to the PDF version?
The Digital Coach’s Bundle now lives in the MBA Toolkit app instead of PDF downloads. You get the same guides plus the quiz games, the animated plays, ready-made practice plans and drills, all on your phone at the field. Already bought the PDF bundle? It’s in the app too, with 90 days of the practice planner free from the first time you open it. Claim your bundle.
Can I cancel the planner anytime?
Yes. Cancel from the Toolkit’s Settings or My Account. You keep the planner until the end of the month you’ve paid for. Payments aren’t refunded for partial months.
Does this work on my phone?
Yes — the app is built mobile-first, since most coaches are pulling up drills and plans from the field, not a desk.
Can I share it with my team?
Yes, with the practice planner (free for your first 3 months). You get 15 Team Passes for your players and assistant coaches. Send your team link and each phone, tablet or computer that opens it gets the quiz games for your guides plus up to 12 situations you pick. Players don’t sign up or give us a name or email, and you can turn off any pass from the app.
How is this different from just Googling drills?
You get a complete system instead of scattered videos: the situations your players need to know, quiz games that make them learn it, and ready-made practices with every drill written out. And with the practice planner, the app diagnoses the actual weakness (one of 400+ specific sub-causes) and builds the practice around fixing it.
Stop guessing what to run at practice.
The situations guides and quiz games, 150 ready-made practice plans and up to 344 drills, on your phone at the field. Pay once and it’s yours.
(function () {
// Old bundle and digital-guide links arrive with ?from=retired (404 redirect snippet).
try {
if (new URLSearchParams(window.location.search).get('from') === 'retired') {
document.getElementById('retired-note').hidden = false;
}
} catch (e) {}
})();
(function(){
var f = document.getElementById("mba8bit-971765"), autoScroll = false;
// Written without the AND operator or the less-than sign on purpose:
// some WordPress themes run the finished page through a text filter that
// HTML-encodes the ampersands inside inline scripts, which breaks the
// whole script (the frame then never resizes or scrolls).
function climbs(n){
if (!n) return false;
if (n === document.body) return false;
return n !== document.documentElement;
}
// Height of any fixed/sticky bar at the top of the page (admin bar, sticky theme header).
function topCover(){
var max = 0;
for (var pass = 0; pass !== 3; pass++){
var y = max + 1, found = max;
[window.innerWidth / 2, 24, window.innerWidth - 24].forEach(function(x){
(document.elementsFromPoint ? document.elementsFromPoint(x, y) : []).forEach(function(el){
for (var n = el; climbs(n); n = n.parentElement){
var cs = getComputedStyle(n);
if (cs.position === 'fixed' || cs.position === 'sticky'){
var r = n.getBoundingClientRect();
if (y >= r.top) { if (window.innerHeight / 2 > r.bottom) found = Math.max(found, r.bottom); }
}
}
});
});
if (found === max) break; max = found;
}
return max;
}
function jump(y){
var top = f.getBoundingClientRect().top + window.pageYOffset + (y || 0) - topCover() - 6;
var still = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;
window.scrollTo({ top: Math.max(0, top), behavior: still ? 'auto' : 'smooth' });
}
window.addEventListener('message', function(e){
if (e.source !== f.contentWindow || !e.data) return;
if (e.data.mba8bit === 'height') f.style.height = (e.data.h + 4) + 'px';
if (e.data.mba8bit === 'scroll') jump(e.data.y);
if (e.data.mba8bit === 'ready') { if (autoScroll) { autoScroll = false; setTimeout(function(){ jump(0); }, 150); } }
});
})();
(function(){
const host = document.getElementById("mba-read-and-run-trial");
const tpl = document.getElementById("mba-read-and-run-trial-tpl");
if(!host || !tpl || host.shadowRoot) return;
const root = host.attachShadow({mode:"open"});
root.appendChild(tpl.content.cloneNode(true));
/* ------------------------------------------------ field geometry (320x240 pixel canvas) */
const cv = root.getElementById("cv"), g = cv.getContext("2d");
const W = 320, H = 240, HX = 160, HY = 210, U = 61;
const P = (r,d) => [HX + r*U*Math.cos(d*Math.PI/180), HY - r*U*Math.sin(d*Math.PI/180)];
const BASES = [[HX,HY], P(1,45), [HX, HY - Math.SQRT2*U], P(1,135), [HX,HY]];
const SIDE = Math.SQRT2*U;
const FIELD = { "1B":[1.15,56], "2B":[1.5,72], "SS":[1.5,108], "3B":[1.15,124], P:[.68,90], LF:[2.25,121], CF:[2.45,90], RF:[2.25,59] };
function fieldersFor(){ const o = {}; for(const k in FIELD) o[k] = P(FIELD[k][0], FIELD[k][1]); o.C = [HX, HY+13]; return o; }
const lerp = (a,b,t) => a+(b-a)*t, clamp01 = t => Math.max(0,Math.min(1,t));
const lerpP = (a,b,t) => [lerp(a[0],b[0],t), lerp(a[1],b[1],t)];
/* ------------------------------------------------ the 3 free-trial situations (the full game has all 46)
ball.k: gb ground ball, ld line drive, pop, fly, hit (single), gap, bunt, dirt (wild pitch), k3 (dropped 3rd strike), steal (no contact)
ball.to: [radius, degrees] from home (45 = 1B line, 90 = CF, 135 = 3B line); f: fielder who plays it; c: caught in the air
runs: [path, start, secondsPerBase, outTime, outBase]. Path numbers are base positions (1.5 = halfway 1st to 2nd).
'wN' waits N seconds; 'u:T' waits until time T. Times: c = contact, p = pitch, k = ball arrives, k+N.
after: throws [from, to, start, arrive]; from 'f' = whoever has the ball. */
const R1 = 1.12, R2 = 2.12, R3 = 3.1;
const DATA = [
{n:6,sec:"RUNNER ON 1ST",title:"Line Drive",runners:[1],outs:1,ballTxt:"Line drive at SS",ball:{k:"ld",to:[1.4,106],f:"SS",c:true},
ok:"Freeze, then go only once it's through",w:["Take off for 2nd on contact","Keep running. Line drives get through"],
why:"On a line drive the ball is either caught or it gets through fast. Freeze first. You only get doubled off if you're moving on a catch.",cue:"Line drive: freeze first, run second.",
g:{runs:[[[R1,1.2,"w.3",1.05],"c"]],say:"CAUGHT. SAFE BACK AT 1ST"},
b:{runs:[[[R1,1.85],"c",.85,1.4,1]],say:"DOUBLED OFF AT 1ST!"}},
{n:18,sec:"RUNNER ON 2ND",title:"Fly Ball, No Outs",runners:[2],outs:0,ballTxt:"Deep fly to RF",ball:{k:"fly",to:[2.75,60],f:"RF",c:true},
ok:"Get back, tag, and go to 3rd after the catch",w:["Go halfway and read it","Run on contact"],
why:"With no outs, getting to 3rd is worth it. From there you can score on a fly ball or a ground ball.",cue:"0 outs, fly ball: tag and take 3rd.",
g:{runs:[[[R2,2,"u:k",3],"c"]],after:[["f",3,2.35,3.2]],say:"TAGGED UP. SAFE AT 3RD!"},
b:{runs:[[[R2,2.5,"u:k",2.05],"c"]],say:"WENT HALFWAY. STUCK AT 2ND"}},
{n:38,sec:"RUNNERS ON 1ST & 3RD",title:"First-and-Third Double Steal",runners:[1,3],outs:1,ballTxt:"R1 steals on the pitch",ball:{k:"steal"},
ok:"R3 goes when the throw passes the pitcher",w:["R3 breaks on the pitch","R3 goes as soon as R1 takes off"],
why:"Trade a stolen base for a run. R1's job is to stay alive. R3 goes only when the throw is past the pitcher and not cut off.",cue:"Past the pitcher? GO.",
g:{runs:[[[R3,3.2,"u:1.05",4],.3],[[R1,1.75,"w.35",1.6,"w.3",1.75],.2]],mv:[["SS",2,.4,1.1]],after:[["C",2,.95,1.45]],say:"DOUBLE STEAL! RUN SCORES!"},
b:{runs:[[[R3,3.75],.2,.85,1.15,3],[[R1,2],.2]],mv:[["3B",3,.3,.9]],say:"BROKE EARLY. PICKED OFF!"}}
];
/* ------------------------------------------------ state */
const TRIAL = [6, 18, 38]; // the same 3 plays every time: line drive, tag-up, double steal
const S = { res:[], mode:"all", len:3, qs:[], i:0, bases:0, runs:0, streak:0, best:0, right:0, missed:[], phase:"title", sound:true, answered:false };
/* ------------------------------------------------ audio (starts only after a click) */
let AC = null;
function ac(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } return AC; }
function tone(f1,f2,dur,gain,type,delay){
const c = ac(); if(!c || !S.sound) return; const t = c.currentTime + (delay||0);
const o = c.createOscillator(), gg = c.createGain(); o.type = type||"square";
o.frequency.setValueAtTime(f1,t); o.frequency.exponentialRampToValueAtTime(f2,t+dur);
gg.gain.setValueAtTime(gain,t); gg.gain.exponentialRampToValueAtTime(0.001,t+dur);
o.connect(gg); gg.connect(c.destination); o.start(t); o.stop(t+dur);
}
function noise(dur,freq,gain,swell){
const c = ac(); if(!c || !S.sound) return;
const len = Math.floor(c.sampleRate*dur), buf = c.createBuffer(1,len,c.sampleRate), d = buf.getChannelData(0);
for(let i=0;i<len;i++) d[i] = (Math.random()*2-1)*Math.pow(1-i/len, swell?0.6:3);
const s = c.createBufferSource(); s.buffer = buf; const f = c.createBiquadFilter(); f.type="bandpass"; f.frequency.value=freq; f.Q.value=.6;
const gg = c.createGain(); gg.gain.value = gain;
if(swell){ gg.gain.setValueAtTime(.001,c.currentTime); gg.gain.linearRampToValueAtTime(gain,c.currentTime+dur*.3); gg.gain.linearRampToValueAtTime(.001,c.currentTime+dur); }
s.connect(f); f.connect(gg); gg.connect(c.destination); s.start();
}
const SFX = {
pitch:()=>noise(.18,1300,.18),
crack:()=>{ noise(.1,3200,1); tone(880,220,.1,.2); },
bunt:()=>{ noise(.06,1600,.5); tone(420,300,.06,.12,"triangle"); },
glove:()=>{ noise(.07,900,.7); tone(170,90,.07,.2,"triangle"); },
whiff:()=>noise(.22,700,.3),
run:()=>{ tone(523,523,.09,.1); tone(659,659,.09,.1,"square",.1); tone(784,784,.18,.1,"square",.2); tone(1047,1047,.25,.1,"square",.34); },
safe:()=>{ tone(784,784,.07,.07); tone(1047,1047,.12,.07,"square",.08); },
out:()=>{ tone(392,196,.3,.12); },
good:()=>{ tone(660,660,.08,.08); tone(880,880,.14,.08,"square",.09); },
bad:()=>{ tone(300,150,.35,.1,"sawtooth"); },
cheer:()=>noise(1.3,1400,.3,true),
blip:()=>tone(660,880,.05,.06)
};
/* ------------------------------------------------ drawing */
const CROWD = []; (function(){ let s = 11; const r = () => (s = (s*16807)%2147483647)/2147483647;
const cols = ["#F4F1E8","#FF914D","#3C5CFC","#C0282D","#FCD800","#9AA6C8","#E3A77F"];
for(let y=6;y<44;y+=4) for(let x=2;x<W;x+=4) if(r()>.28) CROWD.push([x+(r()*2|0),y+(r()*2|0),cols[r()*cols.length|0]]); })();
function px(x,y,w,h,c){ g.fillStyle=c; g.fillRect(Math.round(x),Math.round(y),w,h); }
function drawField(){
px(0,0,W,H,"#05103F");
for(const [x,y,c] of CROWD) px(x,y,2,2,c);
g.fillStyle = "#2E8B3A";
g.beginPath(); g.moveTo(0,H); g.lineTo(0,P(3.05,152)[1]);
for(let d=158; d>=22; d-=2){ const p = P(3.05,d); g.lineTo(p[0],p[1]); }
g.lineTo(W,P(3.05,22)[1]); g.lineTo(W,H); g.closePath(); g.fill();
g.save(); g.clip(); for(let i=0;i<14;i+=2){ g.fillStyle="#349A42"; g.fillRect(0,48+i*14,W,14); } g.restore();
g.strokeStyle = "#FF914D"; g.lineWidth = 2; g.beginPath();
for(let d=158; d>=22; d-=2){ const p = P(3.05,d); d===158 ? g.moveTo(p[0],p[1]) : g.lineTo(p[0],p[1]); } g.stroke();
g.fillStyle = "#C98A4F"; g.beginPath(); g.moveTo(HX,HY);
for(let d=45; d<=135; d+=3){ const p = P(1.6,d); g.lineTo(p[0],p[1]); } g.closePath(); g.fill();
g.beginPath(); g.arc(HX,HY,12,0,Math.PI*2); g.fill();
const i1 = P(.2,45), i2 = [BASES[1][0]-6,BASES[1][1]-1], i3 = [HX,BASES[2][1]+8], i4 = [BASES[3][0]+6,BASES[3][1]-1], i5 = P(.2,135);
g.fillStyle = "#2E8B3A"; g.beginPath(); g.moveTo(i1[0],i1[1]); g.lineTo(i2[0],i2[1]); g.lineTo(i3[0],i3[1]); g.lineTo(i4[0],i4[1]); g.lineTo(i5[0],i5[1]); g.closePath(); g.fill();
const m = P(.68,90); g.fillStyle="#C98A4F"; g.beginPath(); g.arc(m[0],m[1],5,0,Math.PI*2); g.fill(); px(m[0]-2,m[1]-1,4,1,"#F4F1E8");
g.strokeStyle = "#F4F1E8"; g.lineWidth = 1;
const f1 = P(3.05,45), f3 = P(3.05,135);
g.beginPath(); g.moveTo(HX,HY); g.lineTo(f1[0],f1[1]); g.moveTo(HX,HY); g.lineTo(f3[0],f3[1]); g.stroke();
for(let k=1;k<=3;k++){ const b = BASES[k]; px(b[0]-2,b[1]-2,4,4,"#FFFFFF"); }
px(HX-2,HY-1,5,3,"#FFFFFF"); px(HX-1,HY+2,3,1,"#FFFFFF");
g.strokeStyle="rgba(255,255,255,.6)"; g.strokeRect(HX-11.5,HY-4.5,6,10); g.strokeRect(HX+5.5,HY-4.5,6,10);
}
/* 8-bit people: feet at (x,y). "D" defense (gray, red cap), "O" offense (white, navy helmet), "U" umpire */
function guy(x,y,team,frame,alpha,crouch){
x = Math.round(x); y = Math.round(y);
g.globalAlpha = alpha==null ? 1 : alpha;
if(team==="U"){ px(x-2,y-8,4,2,"#191A22"); px(x-2,y-6,4,2,"#E3A77F"); px(x-3,y-4,6,3,"#191A22"); px(x-2,y-1,1,1,"#555"); px(x+1,y-1,1,1,"#555"); g.globalAlpha=1; return; }
const cap = team==="D" ? "#C0282D" : "#1B2A8A", shirt = team==="D" ? "#B9BFCB" : "#F7F7F9", pants = team==="D" ? "#9097A6" : "#E6E8EE";
const top = crouch ? 3 : 0;
px(x-2,y-10+top,4,2,cap); if(team==="O") px(x-2,y-10+top,1,1,"#FF914D");
px(x-2,y-8+top,4,2,"#E3A77F");
px(x-3,y-6+top,6,3,shirt);
if(crouch){ px(x-3,y-3,6,2,pants); }
else if(frame===1){ px(x-3,y-3,2,2,pants); px(x+1,y-3,2,1,pants); px(x+2,y-2,1,1,pants); }
else if(frame===2){ px(x-3,y-3,2,1,pants); px(x-3,y-2,1,1,pants); px(x+1,y-3,2,2,pants); }
else { px(x-2,y-3,2,2,pants); px(x+1,y-3,2,2,pants); }
px(x-3,y-1,2,1,"#191A22"); px(x+1,y-1,2,1,"#191A22");
g.globalAlpha = 1;
}
function batter(x,y,pose,t){
guy(x,y,"O",0,1);
g.strokeStyle = "#DDA862"; g.lineWidth = 2; g.lineCap = "square"; g.beginPath();
const hx = x+3, hy = y-6;
if(pose==="bunt"){ g.moveTo(hx-1,hy+1); g.lineTo(hx+9,hy+1); g.stroke(); return; }
const ang = pose==="swing" ? lerp(-2.2, 0.9, clamp01(t)) : -2.2 + Math.sin(performance.now()*.006)*.12;
g.moveTo(hx,hy); g.lineTo(hx+Math.cos(ang)*9, hy+Math.sin(ang)*9); g.stroke();
}
/* a base marker for a runner who is out: small red X */
function xMark(x,y){ g.strokeStyle="#FF5A4E"; g.lineWidth=1.5; g.beginPath(); g.moveTo(x-4,y-14); g.lineTo(x+4,y-6); g.moveTo(x+4,y-14); g.lineTo(x-4,y-6); g.stroke(); }
/* ------------------------------------------------ play scripts */
const PT0 = .15, PT1 = .65, CT = .72;
const DEF = { gb:[1.15,0], ld:[.92,6], pop:[2.0,55], fly:[2.15,75], hit:[1.45,3], gap:[1.85,10], bunt:[1.05,1] };
function posPt(x){
if(Array.isArray(x)) return x;
if(x <= 0) return BASES[0]; if(x >= 4) return BASES[4];
const i = Math.floor(x), f = x - i; return lerpP(BASES[i], BASES[i+1], f);
}
function ptOf(v, sc, t){ // a throw target: base number, [r,deg], a raw [x,y] point, or a fielder name
if(typeof v === "number") return BASES[v];
if(typeof v === "string") return fielderPos(sc, v, t).p;
return v[0] < 5 ? P(v[0], v[1]) : v;
}
function tok(v, sc){
if(typeof v === "number") return v;
if(v === "c") return CT; if(v === "p") return PT0;
if(v[0] === "k"){ const add = v.length > 1 ? parseFloat(v.slice(1)) : 0; return sc.catchT + add; }
return parseFloat(v);
}
function compileRun(e, sc){
const [path, t0, per=.85, out=null, ob=null] = e;
let t = tok(t0 ?? "c", sc), cur = path[0];
const kf = [{t, x:cur}];
for(const el of path.slice(1)){
if(typeof el === "string"){
if(el[0] === "w"){ t += parseFloat(el.slice(1)); kf.push({t, x:cur}); }
else if(el.startsWith("u:")){ const tt = tok(el.slice(2), sc); if(tt > t){ t = tt; kf.push({t, x:cur}); } }
continue;
}
const a = posPt(cur), b = posPt(el);
const d = (typeof cur === "number" && typeof el === "number") ? Math.abs(el - cur) : Math.hypot(b[0]-a[0], b[1]-a[1]) / SIDE;
t += d * per; kf.push({t, x:el}); cur = el;
}
return { kf, out: out == null ? null : tok(out, sc), ob, start:path[0], end:cur, batter: path[0] === 0 };
}
function runnerAt(r, t){
const kf = r.kf, tt = r.out != null ? Math.min(t, r.out) : t;
if(tt <= kf[0].t) return { p: posPt(kf[0].x), moving:false };
for(let i=1;i<kf.length;i++){
if(tt <= kf[i].t){
const a = kf[i-1], b = kf[i], k = (tt - a.t) / Math.max(.001, b.t - a.t);
const p = (typeof a.x === "number" && typeof b.x === "number") ? posPt(lerp(a.x, b.x, k)) : lerpP(posPt(a.x), posPt(b.x), k);
return { p, moving: a.x !== b.x };
}
}
return { p: posPt(kf[kf.length-1].x), moving:false, done:true };
}
function holderAt(sc, t){ let cur = null; for(const L of sc.ball){ if(L.t0 <= t && (!cur || L.t0 >= cur.t0)) cur = L; } return cur ? cur.to : [HX, HY+8]; }
function fielderPos(sc, name, t){
const start = sc.fielders[name], m = sc.moves[name];
if(!m || t < m.t0) return {p:start, moving:false};
const k = clamp01((t-m.t0)/(m.t1-m.t0));
return {p: lerpP(start, m.to, k), moving: k < 1};
}
function buildScript(sit, correct){
const S2 = correct ? sit.g : sit.b, B = sit.ball;
const sc = { ball:[], moves:{}, runners:[], ev:[], swing:null, bunt:false, fielders:fieldersFor(), catchT:PT1, say:[S2.say, correct ? "good" : "bad"] };
const mound = P(.68,90), plate = [HX+1, HY-4];
const ev = (t,fn) => sc.ev.push({t,fn,done:false});
const move = (name,to,t0,t1) => sc.moves[name] = {to: ptOf(to, sc, t0), t0, t1};
const leg = (from,to,t0,t1,h,kind) => sc.ball.push({from,to,t0,t1,h:h||0,kind});
const batted = ["gb","ld","pop","fly","hit","gap","bunt"].includes(B.k);
// pitch
if(B.k === "dirt" || B.k === "k3"){
leg(mound, [HX,HY+6], PT0, PT1, 2);
leg([HX,HY+6], B.to, PT1, 1.05, 1, "hit"); sc.catchT = 1.05;
move("C", [B.to[0], B.to[1]-2], .8, 1.35); ev(1.35, SFX.glove);
if(B.k === "k3"){ sc.swing = .5; ev(PT1, SFX.whiff); }
} else if(B.k === "steal"){
leg(mound, [HX, HY+8], PT0, PT1, 2); ev(PT1, SFX.glove);
} else {
leg(mound, plate, PT0, PT1, 3);
const [dt, h] = DEF[B.k], t1 = B.t1 || dt, spot = P(B.to[0], B.to[1]);
sc.catchT = t1;
if(B.k === "bunt"){ sc.bunt = true; ev(PT1, SFX.bunt); } else { sc.swing = .5; ev(PT1, SFX.crack); }
leg(plate, spot, PT1, t1, h, "hit");
move(B.f, spot, .7, t1 + (B.k === "hit" || B.k === "gap" ? .12 : 0));
if(B.c) ev(t1, SFX.glove);
}
for(const m of (B.mv||[]).concat(S2.mv||[])) move(m[0], m[1], m[2], m[3]);
for(const [from,to,t0,t1] of (S2.after||[])){
const a = from === "f" ? holderAt(sc, t0) : (from === "C" && !sc.moves.C ? [HX, HY+8] : ptOf(from, sc, t0));
leg(a, ptOf(to, sc, t1), t0, t1, 3, "throw"); ev(t1, SFX.glove);
}
// runners
for(const e of S2.runs){
const r = compileRun(e, sc); sc.runners.push(r);
if(r.out != null && !(S2.after||[]).some(a => Math.abs(a[3] - r.out) < .06)){
const tgt = r.ob != null ? BASES[r.ob] : posPt(r.end), t0 = Math.max(sc.catchT + .05, r.out - .45);
leg(holderAt(sc, t0), tgt, t0, r.out, 3, "throw"); ev(r.out, () => { SFX.glove(); SFX.out(); });
}
}
const nobr = sit.nobr || S2.nobr;
if(batted && !nobr){
sc.runners.push(compileRun([[0,1], .75, 1.0, B.c ? sc.catchT + .2 : null], sc));
}
// bases gained and runs, for scoring
sc.gain = 0; sc.runsIn = 0;
for(const r of sc.runners){
if((r.batter && sit.runners.length) || r.out != null || typeof r.end !== "number" || typeof r.start !== "number") continue;
sc.gain += Math.max(0, Math.floor(r.end + .001) - Math.floor(r.start + .001));
if(r.end >= 4) sc.runsIn++;
}
let end = 2.2;
for(const L of sc.ball) end = Math.max(end, L.t1);
for(const r of sc.runners) end = Math.max(end, r.out != null ? r.out : r.kf[r.kf.length-1].t);
sc.dur = end + 1.1;
return sc;
}
/* ------------------------------------------------ animation */
let scene = null;
function ballPos(sc, t){
let cur = null; for(const L of sc.ball){ if(t >= L.t0 && (!cur || L.t0 >= cur.t0)) cur = L; }
if(!cur) return null;
const k = clamp01((t - cur.t0) / (cur.t1 - cur.t0)), p = lerpP(cur.from, cur.to, k);
return {p, h: cur.h * 4 * k * (1-k)};
}
function startSpots(sit){
return sit.runners.map(b => sit.onbag && b === 3 ? 3 : [0, 1.12, 2.12, 3.1][b]);
}
function draw(now){
drawField();
const sit = scene ? scene.sit : null, sc = scene && scene.sc;
const t = scene && scene.t0 != null ? Math.max(0, (now - scene.t0) / 1000) : 0;
const frame = Math.floor(now/110) % 2 + 1;
const F = sc ? sc.fielders : fieldersFor();
guy(HX, HY+22, "U");
for(const name of ["C","P","1B","2B","SS","3B","LF","CF","RF"]){
const fp = sc ? fielderPos(sc, name, t) : {p:F[name], moving:false};
guy(fp.p[0], fp.p[1] + (name==="C"?2:0), "D", fp.moving ? frame : 0, 1, name==="C" && !fp.moving);
}
if(sc){
let brDrawn = false;
for(const r of sc.runners){
if(r.batter && t < r.kf[0].t) continue;
if(r.batter) brDrawn = true;
const rp = runnerAt(r, t), out = r.out != null && t >= r.out;
if(out && t > r.out + 1.2) continue;
if(!r.batter && r.end >= 4 && rp.done && !out) continue;
guy(rp.p[0], rp.p[1]+3, "O", rp.moving ? frame : 0, out ? .4 : 1);
if(out) xMark(rp.p[0], rp.p[1]+3);
}
if(!brDrawn){
const pose = sc.bunt && t > PT0-.1 ? "bunt" : (sc.swing != null && t > PT1 - .12 ? "swing" : "stance");
batter(HX-9, HY+2, pose, (t - (PT1-.12)) / .18);
}
for(const L of sc.ball){
if(!L.kind || t < L.t0) continue;
const k = clamp01((t - L.t0) / (L.t1 - L.t0)), end = lerpP(L.from, L.to, k);
const len = Math.hypot(end[0]-L.from[0], end[1]-L.from[1]);
for(let d = 0; d < len; d += 4){ const q = lerpP(L.from, end, d/Math.max(len,1)); px(q[0], q[1], 2, 1, L.kind==="hit" ? "#FCD800" : "#F83800"); }
}
const b = ballPos(sc, t);
if(b){ if(b.h > 1) px(b.p[0]-2, b.p[1]+1, 4, 2, "rgba(0,0,0,.5)"); const sz = b.h > 30 ? 4 : 3; px(b.p[0]-1, b.p[1]-b.h-1, sz, sz, "#FFFFFF"); }
} else if(sit){
startSpots(sit).forEach(x => { const p = posPt(x); guy(p[0], p[1]+3, "O", 0); });
batter(HX-9, HY+2, "stance");
}
if(scene && sc && scene.t0 != null){
for(const e of sc.ev){ if(!e.done && t >= e.t){ e.done = true; e.fn(); } }
for(const r of sc.runners){
if(!r.scored && !r.batter && r.end >= 4 && r.out == null && t >= r.kf[r.kf.length-1].t){ r.scored = true; SFX.run(); }
}
if(!scene.said && t >= sc.dur - 1.15){ scene.said = true; flash(sc.say[0], sc.say[1]); if(sc.say[1]==="bad") shake(); }
if(!scene.finished && t >= sc.dur){ scene.finished = true; onPlayDone(); }
}
}
/* Only redraws while the game is on screen: the loop stops when it's scrolled
away or its tab is hidden, and restarts when it comes back. */
let rrOn = true, rrRaf = 0;
function loop(now){ rrRaf = 0; if(!rrOn) return; draw(now); rrRaf = requestAnimationFrame(loop); }
const flashEl = root.getElementById("flash");
function flash(text, cls){ flashEl.className = "flash"; void flashEl.offsetWidth; flashEl.textContent = text; flashEl.className = "flash go " + (cls||""); }
function shake(){ const s = root.getElementById("screen"); s.classList.remove("shake"); void s.offsetWidth; s.classList.add("shake"); }
/* ------------------------------------------------ game flow */
const $ = id => root.getElementById(id);
const ONE = ["BATTER-RUNNER","RUNNER ON 1ST","RUNNER ON 2ND","RUNNER ON 3RD","TWO OUTS"];
function pool(){
if(S.mode === "one") return DATA.filter(d => ONE.includes(d.sec));
if(S.mode === "traffic") return DATA.filter(d => !ONE.includes(d.sec));
return DATA;
}
function shuffle(a){ a = a.slice(); for(let i=a.length-1;i>0;i--){ const j = Math.random()*(i+1)|0; [a[i],a[j]] = [a[j],a[i]]; } return a; }
function runnersTxt(sit){
if(!sit.runners.length) return "Batter only";
if(sit.runners.length === 3) return "Bases loaded";
return sit.runners.map(b => ["","1st","2nd","3rd"][b]).join(" & ");
}
function hudR(sit){
const occ = b => sit.runners.includes(b) ? "r" : "e";
$("hudR").innerHTML = `<span class="lights">OUTS ${[0,1].map(i => `<i class="${i<sit.outs?"o":""}"></i>`).join("")}</span><span class="lights">ON <i class="${occ(3)}"></i><i class="${occ(2)}"></i><i class="${occ(1)}"></i></span>`;
}
function hudL(){
const n = S.qs.length || S.len;
$("pips").innerHTML = `<span>PLAY ${Math.min(S.i+1,n)}/${n}</span>` + Array.from({length:n}, (_,k) => `<i class="${S.res[k]===true?"ok":S.res[k]===false?"no":k===S.i?"cur":""}"></i>`).join("");
$("sBases").textContent = S.bases; $("sStreak").textContent = S.streak;
$("hudL").innerHTML = `BASES <b>${S.bases}</b><br>STREAK <b>${S.streak}</b><br>PLAY <b>${Math.min(S.i+1,S.qs.length||3)}/${S.qs.length||3}</b>`; }
function startGame(){
ac(); SFX.blip();
S.qs = TRIAL.map(n => DATA.find(d => d.n === n)); S.i = 0; S.bases = 0; S.runs = 0; S.streak = 0; S.best = 0; S.right = 0; S.missed = []; S.res = [];
$("start").hidden = true; $("end").hidden = true;
showPlay();
}
function showPlay(){
const sit = S.qs[S.i]; S.answered = false;
scene = { sit, sc:null, t0:null };
$("secTag").textContent = sit.sec;
$("numTag").textContent = `Free trial · guide situation ${sit.n}`;
$("hint").hidden = S.i !== 0;
runnerTags(sit);
$("qTitle").textContent = sit.title;
$("facts").innerHTML = [["RUNNERS", runnersTxt(sit)], ["OUTS", String(sit.outs)], ["THE PLAY", sit.ballTxt]].map(([k,v]) => `<div class="fact"><small>${k}</small>${v}</div>`).join("");
$("prompt").textContent = sit.runners.length ? "WHAT'S THE READ?" : "WHAT DOES THE BATTER-RUNNER DO?";
const opts = shuffle([{t:sit.ok, ok:true}, ...sit.w.map(t => ({t, ok:false}))]); S.opts = opts;
$("opts").innerHTML = opts.map((o,i) => `<button class="opt" data-i="${i}"><span class="key">${i+1}</span><span>${o.t}</span></button>`).join("");
$("result").hidden = true;
hudL(); hudR(sit);
}
/* Label the runners on the field so "R1 / R2 / R3" in the answers is easy to match up. */
function runnerTags(sit){
const one = sit.runners.length <= 1, spots = startSpots(sit);
const at = (p, label) => `<span class="rtag" style="left:${(p[0]/W*100).toFixed(2)}%;top:${((p[1]-9)/H*100).toFixed(2)}%">${label}</span>`;
$("tags").innerHTML = sit.runners.length
? sit.runners.map((b,k) => at(posPt(spots[k]), one ? "YOU" : "R" + b)).join("")
: at([HX-9, HY-4], "YOU");
}
function pick(i){
if(S.answered || S.phase !== "play") return;
S.answered = true;
const o = S.opts[i], sit = S.qs[S.i];
S.res[S.i] = o.ok; $("tags").innerHTML = ""; $("hint").hidden = true;
// stacked layout: bring the field into view so the play is seen, not just the answer
if(host.clientWidth < 860){ try{ $("screen").scrollIntoView({behavior:"smooth", block:"nearest"}); }catch(e){} }
root.querySelectorAll(".opt").forEach((b,k) => { b.disabled = true; b.classList.add(S.opts[k].ok ? "right" : (k===i ? "wrong" : "dim")); });
if(o.ok){ S.right++; S.streak++; S.best = Math.max(S.best, S.streak); SFX.good(); }
else { S.streak = 0; S.missed.push(sit); SFX.bad(); }
scene.sc = buildScript(sit, o.ok); scene.correct = o.ok;
scene.t0 = performance.now() + 250; setTimeout(SFX.pitch, 250 + PT0*1000);
hudL();
}
function onPlayDone(){
const sit = scene.sit, ok = scene.correct, sc = scene.sc;
if(ok){ S.bases += sc.gain; S.runs += sc.runsIn; }
hudL();
$("verdict").className = "verdict " + (ok ? "good" : "bad");
const gainTxt = ok && sc.gain ? ` +${sc.gain} BASE${sc.gain>1?"S":""}` : "";
$("verdict").textContent = ok ? "THAT'S THE READ." + gainTxt : "WRONG READ.";
$("rJob").textContent = sit.ok;
$("rTip").textContent = sit.why;
$("rCue").innerHTML = `<small>FROM THE GUIDE</small>${sit.cue}`;
$("result").hidden = false;
$("nextBtn").textContent = S.i === S.qs.length-1 ? "SEE YOUR SCORE" : "NEXT PLAY";
$("nextBtn").focus({preventScroll:true});
// The verdict/explanation/Next Play button live below the options the
// player just picked from, so once the play finishes animating and this
// result section appears, bring it into view instead of leaving it
// below the fold where it's easy to miss.
setTimeout(() => { try{ $("result").scrollIntoView({behavior:"smooth", block:"nearest"}); }catch(e){} }, 60);
if(ok && sc.runsIn) SFX.cheer(); else if(ok && sc.gain) SFX.safe();
}
function next(){
if(!scene || !scene.finished) return;
SFX.blip();
if(S.i < S.qs.length-1){ S.i++; showPlay(); }
else endGame();
}
function endGame(){
S.phase = "end"; scene = null;
const pct = S.right / S.qs.length;
$("grade").textContent = S.right === 3 ? "BASE BANDIT" : S.right === 2 ? "SPARK PLUG" : S.right === 1 ? "STATION TO STATION" : "PINCH RUNNER NEEDED";
$("endLine").textContent = `${S.right} of ${S.qs.length} reads right. ${S.bases} base${S.bases===1?"":"s"} taken, ${S.runs} run${S.runs===1?"":"s"} scored.`;
$("end").hidden = false; $("subBtn").focus({preventScroll:true});
if(pct >= .7) SFX.cheer();
}
/* ------------------------------------------------ wiring */
// Unlike Know Your Job (which renders inside an <iframe srcdoc>, a fully
// separate document requiring postMessage to talk to the outer page),
// this game's Shadow DOM lives directly in the host page's own document —
// so scrolling the host element into view here is a normal, same-document
// scrollIntoView call, no cross-frame messaging needed.
function scrollHostToTop(){ try{ host.scrollIntoView({behavior:"smooth",block:"start"}); }catch(e){} }
$("playBtn").addEventListener("click", () => { S.phase = "play"; startGame(); setTimeout(scrollHostToTop, 60); });
$("againBtn").addEventListener("click", () => { S.phase = "play"; startGame(); setTimeout(scrollHostToTop, 60); });
$("opts").addEventListener("click", e => { const b = e.target.closest(".opt"); if(b) pick(+b.dataset.i); });
$("nextBtn").addEventListener("click", () => { next(); setTimeout(scrollHostToTop, 60); });
$("mute").addEventListener("click", () => { S.sound = !S.sound; $("mute").textContent = "SOUND: " + (S.sound ? "ON" : "OFF"); $("mute").setAttribute("aria-pressed", String(!S.sound)); });
document.addEventListener("keydown", e => {
const tg = e.target; if(tg && (tg.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(tg.tagName))) return;
if(S.phase === "play" && !S.answered && ["1","2","3"].includes(e.key)) pick(+e.key - 1);
else if(e.key === "Enter" && S.phase === "play" && scene && scene.finished){ e.preventDefault(); next(); }
});
/* No intro screen — the trial starts straight on the first play. */
S.phase = "play";
startGame();
rrRaf = requestAnimationFrame(loop);
if("IntersectionObserver" in window){
new IntersectionObserver(function(es){
rrOn = es[es.length-1].isIntersecting;
if(rrOn){ if(!rrRaf) rrRaf = requestAnimationFrame(loop); }
}, {rootMargin:"200px"}).observe(host);
}
})();
function mbaCtpInit(){
const SECTIONS = [{"id":"A","name":"Winning the Count","items":[{"n":7,"title":"1-2 after two fastballs","count":"1-2","bases":null,"outs":"Any","call":"CH","call14":"CH or BB","loc":"Below the knees","tgt":[[4,2],[4,3]],"avoid":[],"why":"He's seen two fastballs and he's protecting. A changeup after a fastball pattern is the best strikeout pitch in youth baseball.","cue":"Pattern's set. Now change it."}]},{"id":"B","name":"Runners on Base","items":[{"n":12,"title":"Runner on third, less than two outs, infield in","count":"Any","bases":[3],"outs":"0 or 1","call":"FB / CH","call14":null,"loc":"Down. Never up","tgt":[[3,1],[3,2],[3,3]],"avoid":[[1,1],[1,2],[1,3],[0,1],[0,2],[0,3]],"why":"A high pitch becomes a fly ball, and a fly ball is a sacrifice fly. Keep it down and force a ground ball to your drawn-in infield.","cue":"Down! No fly balls."}]},{"id":"E","name":"Bunts, Steals & Plays","items":[{"n":35,"title":"Obvious sacrifice bunt","count":"Any","bases":[1],"outs":"0","call":"FB","call14":null,"loc":"Up in the zone","tgt":[[1,1],[1,2],[1,3]],"avoid":[[3,1],[3,2],[3,3]],"why":"High fastballs are the hardest pitches to bunt down. The bunter pops it up and your infield gets the lead runner.","cue":"Heat up. Pop it up."}]}];
/* Wrong options for every situation, written to be clearly wrong for both age groups. */
const WRONG = {
7:[["Fastball","Third straight one, same spot"],["Fastball","Middle of the plate"]],
12:[["Fastball","Up in the zone"],["Changeup","Up"]],
35:[["Fastball","Down at the knees"],["Changeup","Down"]]
};
/* ------------------------------------------------ data */
const ALL = [];
SECTIONS.forEach(s => s.items.forEach(it => { it.sec = s; ALL.push(it); }));
const expand = c => c.replace(/\bFB\b/g,"Fastball").replace(/\bCH\b/g,"Changeup").replace(/\bBB\b/g,"Breaking ball")
.replace(/ \/ /g," or ").replace(/^(\w)/, m => m.toUpperCase());
/* ------------------------------------------------ state */
const S = { age:"y", len:3, qs:[], i:0, k:0, streak:0, best:0, missed:[], phase:"title", sound:true };
try { const a = localStorage.getItem("ctp_age"); if (a==="y"||a==="o") S.age=a; } catch(e){}
/* ------------------------------------------------ audio */
let AC = null;
function ac(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } return AC; }
function noise(dur, freq, q, gain, type){
const c = ac(); if(!c || !S.sound) return;
const len = Math.floor(c.sampleRate*dur), buf = c.createBuffer(1,len,c.sampleRate), d = buf.getChannelData(0);
for(let i=0;i<len;i++) d[i] = (Math.random()*2-1)*Math.pow(1-i/len, type==="swell"?0.6:3);
const src = c.createBufferSource(); src.buffer = buf;
const f = c.createBiquadFilter(); f.type = "bandpass"; f.frequency.value = freq; f.Q.value = q;
const g = c.createGain(); g.gain.value = gain;
if(type==="swell"){ g.gain.setValueAtTime(0.001,c.currentTime); g.gain.linearRampToValueAtTime(gain,c.currentTime+dur*0.3); g.gain.linearRampToValueAtTime(0.001,c.currentTime+dur); }
src.connect(f); f.connect(g); g.connect(c.destination); src.start();
}
function tone(f1,f2,dur,gain,type){
const c = ac(); if(!c || !S.sound) return;
const o = c.createOscillator(), g = c.createGain(); o.type = type||"square";
o.frequency.setValueAtTime(f1,c.currentTime); o.frequency.exponentialRampToValueAtTime(f2,c.currentTime+dur);
g.gain.setValueAtTime(gain,c.currentTime); g.gain.exponentialRampToValueAtTime(0.001,c.currentTime+dur);
o.connect(g); g.connect(c.destination); o.start(); o.stop(c.currentTime+dur);
}
const SFX = {
whoosh:()=>noise(.25,1200,.7,.25),
pop:()=>{ noise(.09,900,1.2,.9); tone(180,90,.08,.25,"triangle"); },
crack:()=>{ noise(.12,3200,.8,1.2); tone(900,200,.12,.25); },
cheer:()=>noise(1.6,1400,.4,.35,"swell"),
groan:()=>noise(1.1,500,.6,.3,"swell"),
strike:()=>{ tone(520,520,.12,.14); setTimeout(()=>tone(780,780,.22,.14),130); },
blip:()=>tone(660,880,.06,.08)
};
/* ------------------------------------------------ canvas scene: 480x270 logical units, rendered at 3x */
const cv = document.getElementById("cv"), g = cv.getContext("2d");
const W = 480, H = 270, SC = 3;
cv.width = W*SC; cv.height = H*SC;
g.imageSmoothingEnabled = true;
const C = { navy:"#000D49", navy2:"#0A1A66", navy3:"#16287F", or:"#FF914D", ord:"#E8691C", chalk:"#F4F1E8", out:"#0B0E24",
g1:"#2C8538", g2:"#379648", dirt:"#C98A4F", dirtL:"#D9A067", dirtD:"#A56B3A",
skin:"#E3A77F", skinD:"#B97E57", white:"#F7F7F9", gray:"#B9BFCB", red:"#C0282D", black:"#191A22", glove:"#8E5528", wood:"#DDA862" };
const lerp = (a,b,t) => a + (b-a)*t;
const clamp01 = t => Math.max(0, Math.min(1, t));
const ease = t => t<.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2;
function shade(hex, amt){ const n = parseInt(hex.slice(1),16); let r=n>>16, gg=(n>>8)&255, b=n&255;
const f = v => Math.max(0, Math.min(255, Math.round(amt<0 ? v*(1+amt) : v+(255-v)*amt)));
return "rgb(" + f(r) + "," + f(gg) + "," + f(b) + ")"; }
const LX = -0.55, LY = -0.83; // key light from the upper-left light tower
/* ---------- shaded, tapered figure parts ---------- */
function capsule(ctx, a, b, r1, r2){
const dx=b[0]-a[0], dy=b[1]-a[1], L=Math.hypot(dx,dy)||1, nx=-dy/L, ny=dx/L;
const q = [[a[0]+nx*r1, a[1]+ny*r1],[b[0]+nx*r2, b[1]+ny*r2],[b[0]-nx*r2, b[1]-ny*r2],[a[0]-nx*r1, a[1]-ny*r1]];
let area = 0; for(let i=0;i<4;i++){ const p=q[i], n=q[(i+1)%4]; area += p[0]*n[1]-n[0]*p[1]; }
const ccw = area < 0; // wind the end caps the same way as the body so overlaps fill solid
ctx.moveTo(q[0][0],q[0][1]); for(let i=1;i<4;i++) ctx.lineTo(q[i][0],q[i][1]); ctx.closePath();
ctx.moveTo(a[0]+r1, a[1]); ctx.arc(a[0],a[1],r1,0,Math.PI*2,ccw);
ctx.moveTo(b[0]+r2, b[1]); ctx.arc(b[0],b[1],r2,0,Math.PI*2,ccw);
return [nx,ny];
}
function limbGrad(a,b,r,col){
const dx=b[0]-a[0], dy=b[1]-a[1], L=Math.hypot(dx,dy)||1; let nx=-dy/L, ny=dx/L;
if(nx*LX+ny*LY < 0){ nx=-nx; ny=-ny; }
const mx=(a[0]+b[0])/2, my=(a[1]+b[1])/2;
const gr = g.createLinearGradient(mx+nx*r, my+ny*r, mx-nx*r, my-ny*r);
gr.addColorStop(0, shade(col,.28)); gr.addColorStop(.45, col); gr.addColorStop(1, shade(col,-.34));
return gr;
}
function polyGrad(pts,col){
const xs=pts.map(p=>p[0]), ys=pts.map(p=>p[1]);
const gr = g.createLinearGradient(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys));
gr.addColorStop(0, shade(col,.22)); gr.addColorStop(.5, col); gr.addColorStop(1, shade(col,-.32)); return gr;
}
function ballGrad(x,y,r,col){ const gr = g.createRadialGradient(x+LX*r*.5, y+LY*r*.5, r*.1, x, y, r*1.05);
gr.addColorStop(0, shade(col,.35)); gr.addColorStop(.55, col); gr.addColorStop(1, shade(col,-.4)); return gr; }
const OUTW = 0.9;
function drawParts(parts){
for(const p of parts){ // outline pass
g.fillStyle = C.out; g.beginPath();
if(p.k==="L") capsule(g, p.a, p.b, p.r1+OUTW, p.r2+OUTW);
else if(p.k==="E") g.ellipse(p.x,p.y,p.rx+OUTW,p.ry+OUTW,p.rot||0,0,Math.PI*2);
else if(p.k==="P"){ g.lineWidth = OUTW*2; g.lineJoin="round"; g.strokeStyle=C.out; p.pts.forEach((q,i)=> i?g.lineTo(q[0],q[1]):g.moveTo(q[0],q[1])); g.closePath(); g.stroke(); continue; }
g.fill();
}
for(const p of parts){ // shaded fill pass
g.beginPath();
if(p.k==="L"){ capsule(g, p.a, p.b, p.r1, p.r2); g.fillStyle = limbGrad(p.a,p.b,Math.max(p.r1,p.r2),p.c); }
else if(p.k==="E"){ g.ellipse(p.x,p.y,p.rx,p.ry,p.rot||0,0,Math.PI*2); g.fillStyle = ballGrad(p.x,p.y,Math.max(p.rx,p.ry),p.c); }
else if(p.k==="P"){ p.pts.forEach((q,i)=> i?g.lineTo(q[0],q[1]):g.moveTo(q[0],q[1])); g.closePath(); g.fillStyle = polyGrad(p.pts,p.c); }
g.fill();
if(p.after) p.after();
}
}
const Lb = (a,b,r1,r2,c,after) => ({k:"L",a,b,r1,r2,c,after});
const El = (x,y,rx,ry,c,rot,after) => ({k:"E",x,y,rx,ry,c,rot,after});
const Pg = (pts,c,after) => ({k:"P",pts,c,after});
/* soft double shadows from the two light towers */
function shadows(x,y,w,a){
g.fillStyle = "rgba(20,10,0," + (a||.28) + ")";
g.beginPath(); g.ellipse(x+w*.35,y,w,w*.2,.25,0,Math.PI*2); g.fill();
g.beginPath(); g.ellipse(x-w*.35,y,w*.9,w*.18,-.25,0,Math.PI*2); g.fill();
}
function lerpPose(A,B,t){ const o={}; for(const k in A){ o[k] = Array.isArray(A[k]) ? [lerp(A[k][0],B[k][0],t), lerp(A[k][1],B[k][1],t)] : lerp(A[k],B[k],t); } return o; }
function poseAt(frames, f){ const i = Math.max(0, Math.min(frames.length-2, Math.floor(f))); return lerpPose(frames[i], frames[i+1], clamp01(f-i)); }
/* ---------- pitcher: right-hander seen from center field.
0 set (side-on, facing 1B = screen left) · 1 leg lift · 2 stride (opening) · 3 release (back to camera) · 4 follow-through */
const PITCH_POSES = [
{face:0, head:[150,138],neck:[155,150],shL:[152,155],shR:[160,157],elL:[146,170],hL:[139,176],elR:[152,173],hR:[141,176],hipL:[154,197],hipR:[160,199],knL:[151,226],ftL:[152,247],knR:[163,230],ftR:[161,259]},
{face:0, head:[153,136],neck:[157,148],shL:[155,153],shR:[163,155],elL:[148,166],hL:[141,170],elR:[155,170],hR:[143,170],hipL:[156,195],hipR:[162,197],knL:[138,186],ftL:[146,212],knR:[163,229],ftR:[161,259]},
{face:.35, head:[152,142],neck:[155,154],shL:[145,157],shR:[166,159],elL:[131,151],hL:[123,139],elR:[180,168],hR:[193,156],hipL:[149,200],hipR:[163,202],knL:[146,216],ftL:[150,234],knR:[167,231],ftR:[165,259]},
{face:1, head:[149,141],neck:[150,153],shL:[134,158],shR:[165,154],elL:[127,170],hL:[137,181],elR:[174,131],hR:[162,111],hipL:[144,202],hipR:[162,203],knL:[146,219],ftL:[150,236],knR:[170,230],ftR:[176,256]},
{face:1, head:[145,147],neck:[147,159],shL:[131,165],shR:[160,161],elL:[124,178],hL:[133,188],elR:[142,186],hR:[128,210],hipL:[143,204],hipR:[161,204],knL:[146,221],ftL:[150,236],knR:[166,221],ftR:[178,236]}
];
(function(){ const cx = 156; // right-hander faces third base (screen right) in the set
for(let i=0;i<3;i++){ const p = PITCH_POSES[i]; for(const k in p){ if(Array.isArray(p[k])) p[k] = [2*cx - p[k][0], p[k][1]]; } } })();
function drawPitcher(f, now){
const P = poseAt(PITCH_POSES, f), face = P.face;
if(f === 0){ const b = Math.sin(now*0.003)*0.6; ["head","neck","shL","shR","elL","hL","elR","hR"].forEach(k => P[k][1] += b); }
shadows((P.ftL[0]+P.ftR[0])/2, 262, 30, .3);
const J = C.white, sock = (k,f2,t) => [lerp(k[0],f2[0],t), lerp(k[1],f2[1],t)];
const legs = side => { const hip=P["hip"+side], kn=P["kn"+side], ft=P["ft"+side], s1=sock(kn,ft,.5);
const toe = face < .5 ? 4 : (side==="L"?-1:1); // in the set his toes point toward third base
return [Lb(hip,kn,8.2,6.6,J,() => pinstripe(hip,kn)), Lb(kn,s1,6.4,5.2,J), Lb(s1,ft,5.0,4.2,C.navy), El(ft[0]+toe,ft[1]+1.5,8.5,4.2,C.black)]; };
// torso thickness: narrow side-on, full width from behind
const sc = [(P.shL[0]+P.shR[0])/2, (P.shL[1]+P.shR[1])/2], hc = [(P.hipL[0]+P.hipR[0])/2, (P.hipL[1]+P.hipR[1])/2];
const ws = lerp(12, 19, face), wh = lerp(11, 16, face), lean = (P.shR[1]-P.shL[1])*.5;
const torso = [[sc[0]-ws, sc[1]-2-lean],[P.neck[0],P.neck[1]-3],[sc[0]+ws+(face<.5?4:0), sc[1]-2+lean],[hc[0]+wh+(face<.5?3:0), hc[1]+2],[hc[0]-wh, hc[1]+2]];
const gloveArm = [Lb(P.shL,P.elL,5.6,4.6,J), Lb(P.elL,P.hL,4.4,3.8,C.navy)];
const throwArm = [Lb(P.shR,P.elR,5.6,4.6,J), Lb(P.elR,P.hR,4.4,3.8,C.navy)];
const glove = El(P.hL[0],P.hL[1],7.8,8.8,C.glove,.3,() => gloveDetail(P.hL[0],P.hL[1],7.8));
const ballHand = El(P.hR[0],P.hR[1],3.6,3.6,C.skin);
const parts = [...legs("L"), ...legs("R")];
const finish = f > 3.15; // follow-through: throwing arm comes across the FRONT of his body
if(face < .5) parts.push(...gloveArm); // far arm hides behind his body side-on
if(finish) parts.push(...throwArm, ballHand); // hidden by his back from this angle
parts.push(Pg(torso, J, () => {
const beltY = hc[1]-1;
g.fillStyle = C.navy; g.fillRect(hc[0]-wh, beltY-2, (wh*2)+(face<.5?3:0), 3.6);
g.strokeStyle = C.or; g.lineWidth = 1.3; g.beginPath(); g.moveTo(torso[0][0],torso[0][1]+1); g.lineTo(P.neck[0],P.neck[1]-2); g.lineTo(torso[2][0],torso[2][1]+1); g.stroke();
g.strokeStyle = "rgba(90,100,130,.25)"; g.lineWidth = .9;
g.beginPath(); g.moveTo(sc[0]-ws*.5,sc[1]+8); g.quadraticCurveTo(sc[0]-2,lerp(sc[1],hc[1],.5),hc[0]-wh*.5,hc[1]-6); g.stroke();
if(face > .3){ // number turns into view as he opens up
const cx = lerp(sc[0],hc[0],.45), cy = lerp(sc[1],hc[1],.46), ang = Math.atan2(P.shR[1]-P.shL[1], P.shR[0]-P.shL[0]);
g.save(); g.translate(cx,cy); g.rotate(ang*face); g.scale(Math.max(.15,face),1); g.globalAlpha = clamp01((face-.3)*2);
g.textAlign="center"; g.textBaseline="middle"; g.font = "900 20px 'Arial Black',Arial,sans-serif";
g.lineWidth = 2.2; g.strokeStyle = C.or; g.strokeText("12",0,4); g.fillStyle = C.navy; g.fillText("12",0,4);
g.font = "bold 6.5px Arial,sans-serif"; g.fillText("M B A",0,-10); g.restore();
} else { // side-on: sleeve number on the near arm
g.fillStyle = C.or; g.fillRect(P.shR[0]-3, P.shR[1]+3, 5, 2);
}
}));
parts.push(Lb(P.neck,[P.head[0]-(face<.5?3:0),P.head[1]+6],4,4,C.skinD));
parts.push(El(P.head[0],P.head[1],10.4,10.8,face<.5?C.skinD:"#3B2618",0,() => headPitcher(P.head, face)));
if(face < .5) parts.push(...throwArm, ballHand, glove); // hands come together in front of his chest
else if(finish) parts.push(...gloveArm, glove);
else parts.push(...gloveArm, glove, ...throwArm, ballHand);
drawParts(parts);
}
function pinstripe(a,b){ g.save(); g.strokeStyle = "rgba(0,13,73,.16)"; g.lineWidth = .45;
for(const o of [-4,0,4]){ g.beginPath(); g.moveTo(a[0]+o,a[1]); g.lineTo(b[0]+o*.8,b[1]); g.stroke(); } g.restore(); }
function gloveDetail(x,y,r){
g.strokeStyle = "rgba(40,18,5,.55)"; g.lineWidth = .6;
for(let i=-2;i<=2;i++){ g.beginPath(); g.moveTo(x-r*.7, y+i*2.2); g.lineTo(x+r*.2, y+i*2.2-1); g.stroke(); } // web lacing
g.strokeStyle = "rgba(255,220,180,.35)"; g.beginPath(); g.arc(x,y,r*.75,-2.4,-1.2); g.stroke();
}
function headPitcher(h, face){
const [x,y] = h;
if(face < .5){
// side-on, facing screen left: hair at the back, ear, nose, jaw
g.fillStyle = "#3B2618"; g.beginPath(); g.ellipse(x-4,y+1,6.5,8.5,0,Math.PI-1.8,Math.PI+1.4); g.fill(); // hair at the back
g.fillStyle = shade(C.skinD,-.12); g.beginPath(); g.ellipse(x-3,y+1.5,2,3,0,0,Math.PI*2); g.fill(); // ear
g.fillStyle = C.skinD; g.beginPath(); g.moveTo(x+9.6,y+1); g.lineTo(x+12.4,y+3.6); g.lineTo(x+9.4,y+4.6); g.fill(); // nose toward third
g.fillStyle = "#1A1A22"; g.beginPath(); g.arc(x+6.5,y+.5,.9,0,Math.PI*2); g.fill(); // eye
} else {
g.fillStyle = shade(C.skinD,-.1); g.beginPath(); g.ellipse(x-10,y+1,2,3.2,0,0,Math.PI*2); g.ellipse(x+10,y+1,2,3.2,0,0,Math.PI*2); g.fill();
}
const cg = g.createLinearGradient(x-10,y-12,x+10,y); cg.addColorStop(0,"#1B2B7A"); cg.addColorStop(1,"#00072E");
g.fillStyle = cg; g.beginPath(); g.ellipse(x,y-2.5,11.6,9.6,0,Math.PI,0); g.lineTo(x+11.6,y-1); g.lineTo(x-11.6,y-1); g.fill();
const brim = (1-face)*9; // bill points toward third base when side-on
if(brim > .5){ g.fillStyle = "#050B35"; g.beginPath(); g.moveTo(x+10,y-2.4); g.lineTo(x+10+brim,y-.2); g.lineTo(x+10+brim*.8,y+1); g.lineTo(x+9,y); g.fill(); }
g.fillStyle = C.or; g.beginPath(); g.arc(x,y-12,1.2,0,Math.PI*2); g.fill();
if(face > .5) g.fillRect(x-3,y-2.4,6,1.8);
g.fillStyle = "rgba(255,255,255,.18)"; g.beginPath(); g.ellipse(x-4,y-8,4,2,-.3,0,Math.PI*2); g.fill();
}
/* ---------- batter: right-handed, side-on to the plate facing screen left.
His front shoulder and front foot point at the pitcher (toward the camera = down the screen). */
/* Swing keyframes, drawn as a right-hander facing screen left, then mirrored into a left-handed hitter.
bat angle: -PI/2 = straight up, -PI = level toward the plate. len shrinks when the barrel points toward/away from the camera. */
const BAT_KEYS = [
[0, {open:0, len:26, head:[263,68],shF:[263,81],shB:[270,77],elF:[259,84],elB:[270,82],hands:[266,70],hipF:[266,99],hipB:[270,95],knF:[263,113],ftF:[265,128],knB:[275,108],ftB:[273,118],bat:-1.15}], // stance
[.18, {open:0, len:25, head:[263,68],shF:[264,81],shB:[271,77],elF:[261,83],elB:[273,80],hands:[269,69],hipF:[267,98],hipB:[271,95],knF:[261,109],ftF:[263,121],knB:[275,108],ftB:[273,118],bat:-1.08}], // load: hands drift back, front foot lifts
[.36, {open:.45, len:23, head:[262,69],shF:[261,82],shB:[271,79],elF:[257,86],elB:[268,92],hands:[262,86],hipF:[262,100],hipB:[271,97],knF:[258,116],ftF:[259,130],knB:[271,110],ftB:[273,119],bat:-1.62}], // slot: foot down, hips open, barrel above hands
[.5, {open:.8, len:31, head:[260,70],shF:[258,82],shB:[270,80],elF:[255,89],elB:[264,92],hands:[252,93],hipF:[261,100],hipB:[270,97],knF:[258,116],ftF:[259,130],knB:[269,110],ftB:[273,119],bat:-Math.PI-0.08}], // contact: level through the zone
[.7, {open:.95, len:13, head:[259,70],shF:[256,82],shB:[268,81],elF:[250,86],elB:[258,88],hands:[247,86],hipF:[261,100],hipB:[269,97],knF:[258,116],ftF:[259,130],knB:[267,111],ftB:[272,119],bat:-2.55}], // extension toward the pitcher
[1, {open:1, len:23, head:[258,70],shF:[256,80],shB:[267,80],elF:[253,76],elB:[259,78],hands:[257,69],hipF:[261,100],hipB:[269,97],knF:[258,116],ftF:[259,130],knB:[266,111],ftB:[271,119],bat:-0.55}] // finish over the front shoulder
];
const BAT_POSES = [BAT_KEYS[0][1], BAT_KEYS[3][1], BAT_KEYS[5][1]];
function batPose(t){
t = clamp01(t);
let i = 0; while(i < BAT_KEYS.length-2 && t > BAT_KEYS[i+1][0]) i++;
const [t0,A] = BAT_KEYS[i], [t1,B] = BAT_KEYS[i+1], k = clamp01((t-t0)/(t1-t0));
const P = lerpPose(A, B, ease(k));
if(i === 2) P.bat = lerp(A.bat, B.bat, Math.pow(k, 1.7)); // barrel lags the hands, then whips into the zone
return P;
}
function drawBatter(t, now){
let P = batPose(t);
if(scene.bunt > 0) P = lerpPose(P, BUNT_POSE, scene.bunt);
if(t === 0 && !scene.bunt){ const w = Math.sin(now*0.005); P.bat += w*0.12; P.hands[0] += w*0.6; P.hands[1] += Math.cos(now*0.005)*0.4; }
shadows(268,126,14,.26);
if(t > .3 && t < .8 && !(scene.bunt > .3)){
for(const [dt,al] of [[.03,.22],[.06,.14],[.09,.08]]){
const Q = batPose(t-dt), L = Q.len || 30;
g.strokeStyle = `rgba(221,168,98,${al})`; g.lineCap = "round"; g.lineWidth = 3.4;
g.beginPath(); g.moveTo(Q.hands[0]+Math.cos(Q.bat)*L*.35, Q.hands[1]+Math.sin(Q.bat)*L*.35); g.lineTo(Q.hands[0]+Math.cos(Q.bat)*L, Q.hands[1]+Math.sin(Q.bat)*L); g.stroke();
}
}
const bL = P.len || 30, be = [P.hands[0]+Math.cos(P.bat)*bL, P.hands[1]+Math.sin(P.bat)*bL], bm = [P.hands[0]+Math.cos(P.bat)*bL*.5, P.hands[1]+Math.sin(P.bat)*bL*.5];
const behind = t > .75 && Math.sin(P.bat) < -0.35 && Math.cos(P.bat) > -0.15; // only the finish wraps behind his head
const bat = [Lb(P.hands,bm,1.1,1.4,C.wood,() => { g.strokeStyle = C.black; g.lineWidth = 2.4; g.beginPath(); g.moveTo(P.hands[0],P.hands[1]); g.lineTo(lerp(P.hands[0],bm[0],.35),lerp(P.hands[1],bm[1],.35)); g.stroke(); }),
Lb(bm,be,1.5,2.1,C.wood)];
const sc = [(P.shF[0]+P.shB[0])/2,(P.shF[1]+P.shB[1])/2], hc = [(P.hipF[0]+P.hipB[0])/2,(P.hipF[1]+P.hipB[1])/2];
const ws = lerp(5.5, 8, P.open), wh = lerp(5, 6.5, P.open);
const parts = [];
if(behind) parts.push(...bat);
parts.push(Lb(P.hipB,P.knB,3.3,2.7,C.gray), Lb(P.knB,P.ftB,2.7,2.1,C.gray), El(P.ftB[0]-1.5,P.ftB[1],3.8,1.8,C.black)); // back leg (farther away)
parts.push(Pg([[sc[0]-ws,sc[1]-1],[sc[0]+ws,sc[1]-2],[hc[0]+wh,hc[1]],[hc[0]-wh,hc[1]+1]], C.gray, () => {
g.strokeStyle = C.red; g.lineWidth = 1.5; g.beginPath(); g.moveTo(hc[0]-wh,hc[1]-1); g.lineTo(hc[0]+wh,hc[1]-2); g.stroke();
g.strokeStyle = C.red; g.lineWidth = .8; g.beginPath(); g.moveTo(sc[0]-ws+1,sc[1]); g.lineTo(sc[0]-ws+2,hc[1]-2); g.stroke(); // placket piping
if(P.open > .4){ g.globalAlpha = clamp01((P.open-.4)*2); g.fillStyle = C.red; g.font = "bold 5.5px Arial"; g.textAlign = "center"; g.save(); g.translate(sc[0], lerp(sc[1],hc[1],.5)+2); g.scale(-1,1); g.fillText("7",0,0); g.restore(); g.globalAlpha = 1; }
}));
parts.push(Lb(P.hipF,P.knF,3.5,2.9,C.gray), Lb(P.knF,P.ftF,2.9,2.3,C.gray), El(P.ftF[0]-2,P.ftF[1]+.5,4.3,2.1,C.black)); // front leg (closer)
const arms = [Lb(P.shB,P.elB,2.2,1.9,C.gray), Lb(P.elB,P.hands,1.8,1.5,C.skinD),
Lb(P.shF,P.elF,2.4,2.1,C.gray), Lb(P.elF,P.hands,2,1.7,C.skinD), El(P.hands[0],P.hands[1],2.4,2.4,C.white)];
const head = El(P.head[0],P.head[1],5,5.3,C.skinD,0,() => headBatter(P.head, P.open));
const loaded = t < .22 && !(scene.bunt > .3); // loaded hands tuck in behind his head in profile
if(loaded){ parts.push(...arms); if(!behind) parts.push(...bat); parts.push(head); }
else { parts.push(head, ...arms); if(!behind) parts.push(...bat); }
drawParts(parts);
}
function headBatter(h, open){
const [x,y] = h;
g.fillStyle = C.skinD; g.beginPath(); g.moveTo(x-4.6,y); g.lineTo(x-6.4,y+1.8); g.lineTo(x-4.4,y+2.4); g.fill(); // nose, facing the plate
g.fillStyle = "#1A1A22"; g.beginPath(); g.arc(x-3,y-.2,.55,0,Math.PI*2); g.fill();
g.fillStyle = ballGrad(x,y-2,6,C.red); g.beginPath(); g.ellipse(x+.6,y-1.3,6,5.2,0,Math.PI,0); g.lineTo(x+6.6,y+1); g.lineTo(x-5.4,y+1); g.fill();
g.fillStyle = shade(C.red,-.22); g.beginPath(); g.moveTo(x-5,y-1.6); g.lineTo(x-9.5,y-.6); g.lineTo(x-9,y+.3); g.lineTo(x-4.6,y-.2); g.fill(); // bill
g.fillStyle = shade(C.red,-.1); g.beginPath(); g.ellipse(x+1.2,y+1.4,2.6,3.2,0,0,Math.PI*2); g.fill(); // ear flap toward the pitcher
g.fillStyle = "rgba(255,255,255,.5)"; g.beginPath(); g.ellipse(x-.5,y-4.6,2,.9,-.3,0,Math.PI*2); g.fill();
}
/* ---------- catcher: crouched behind the plate, farther from the camera ---------- */
const CAT_S = .9, CAT_Y = 112; // feet sit behind the plate's back point (y 127) and higher on screen
const catT = p => [240 + (p[0]-240)*CAT_S, CAT_Y + (p[1]-124)*CAT_S];
function drawCatcher(mx,my){
shadows(240,CAT_Y+1,12,.3);
g.save(); g.translate(240,CAT_Y); g.scale(CAT_S,CAT_S); g.translate(-240,-124);
drawParts([
Lb([236,116],[228,114],3.8,3.4,C.gray), Lb([228,114],[232,124],3.4,2.8,C.navy2), El(232,124.5,4,2,C.black),
Lb([244,116],[252,114],3.8,3.4,C.gray), Lb([252,114],[248,124],3.4,2.8,C.navy2), El(248,124.5,4,2,C.black),
Pg([[231,102],[249,102],[248,117],[232,117]], "#22306A", () => { g.fillStyle=C.or; g.fillRect(238.6,104,2.8,11); g.strokeStyle="rgba(255,255,255,.15)"; g.lineWidth=.5; for(let y=106;y<116;y+=3){ g.beginPath(); g.moveTo(232,y); g.lineTo(248,y); g.stroke(); } }),
Lb([232,104],[229,113],2.6,2.2,C.navy2), El(229,114,2.4,2.4,C.skinD),
El(240,96,5.6,6,C.navy,0,() => { g.strokeStyle="#AEB6C8"; g.lineWidth=.7; for(let i=0;i<4;i++){ g.beginPath(); g.moveTo(236,93.5+i*1.8); g.lineTo(244,93.5+i*1.8); g.stroke(); } g.beginPath(); g.moveTo(240,92); g.lineTo(240,101); g.stroke(); })
]);
g.restore();
// glove arm reaches forward to the target in world space
const sh = catT([248,104]), el = [lerp(sh[0],mx,.45)+2.5, lerp(sh[1],my,.45)+1.5];
drawParts([ Lb(sh,el,2.4,2.1,C.navy2), Lb(el,[mx,my],2.1,1.9,C.navy2),
El(mx,my,5.2,5.7,C.glove,0,() => { g.fillStyle = "rgba(60,30,10,.7)"; g.beginPath(); g.arc(mx-.5,my,2.2,0,Math.PI*2); g.fill(); gloveDetail(mx,my,4.4); }) ]);
}
/* ---------- umpire: set up behind the catcher, looking over his shoulder ---------- */
const UMP_S = .84, UMP_Y = 104;
function drawUmp(t){
const up = ease(clamp01(t));
const el = [lerp(229,222,up), lerp(100,80,up)], fist = [lerp(232,227,up), lerp(106,66,up)];
shadows(240,UMP_Y+1,11,.22);
g.save(); g.translate(240,UMP_Y); g.scale(UMP_S,UMP_S); g.translate(-240,-124);
drawParts([
Lb([234,106],[233,122],3.6,3,C.grayS||"#858C9E"), Lb([246,106],[247,122],3.6,3,"#858C9E"), El(232,123,4,2,C.black), El(248,123,4,2,C.black),
Pg([[229,89],[251,89],[249,106],[231,106]], C.black),
Lb([249,90],[251,104],2.9,2.5,C.black), El(250,106,2.4,2.4,C.skinD),
Lb([231,90],el,2.9,2.6,C.black), Lb(el,fist,2.6,2.4,C.black), El(fist[0],fist[1],2.8,2.8,C.skinD),
El(240,80,6,6.3,C.black,0,() => { g.strokeStyle="#8A91A6"; g.lineWidth=.6; for(let i=0;i<3;i++){ g.beginPath(); g.moveTo(236,79+i*2); g.lineTo(244,79+i*2); g.stroke(); } g.fillStyle="#2E3142"; g.fillRect(234,73,12,2.6); })
]);
g.restore();
}
/* ---------- crowd: pre-rendered seated + two cheering variants, with depth blur ---------- */
function makeCrowd(variant){
const c = document.createElement("canvas"); c.width = W*SC; c.height = 75*SC;
const x = c.getContext("2d"); x.scale(SC,SC);
let seed = 11; const rnd = () => (seed = (seed*16807)%2147483647)/2147483647;
const shirts = ["#FF914D","#F4F1E8","#3C6FD8","#E14B4B","#F6C744","#5BE08C","#FFFFFF","#9B7BE0","#FF914D","#000D49","#2BB3C0"];
const skins = ["#E2A57C","#C98A64","#8C5A3C","#F0C29E","#6E4630"];
/* Each deck of fans is drawn sharp onto a scratch layer, then blurred onto the
crowd in ONE pass. Blurring every fan separately looked the same but took
seconds on a phone, which left the game on a black screen. */
const L = document.createElement("canvas"); L.width = c.width; L.height = c.height;
const lx = L.getContext("2d"), crowdX = x;
const deck = (y0, rows, size, gap, blur, gapFn) => {
const x = lx;
x.setTransform(1,0,0,1,0,0); x.clearRect(0,0,L.width,L.height); x.setTransform(SC,0,0,SC,0,0);
for(let r=0;r<rows;r++){ const y = y0 + r*size*1.45;
for(let px=(r%2)*gap/2; px<W; px+=gap){
if(gapFn && gapFn(px,r)) { rnd(); rnd(); rnd(); rnd(); continue; }
const shirt = shirts[Math.floor(rnd()*shirts.length)], skin = skins[Math.floor(rnd()*skins.length)], cap = rnd()<.3, jumpy = rnd();
const up = variant && ((variant===1 && jumpy<.6) || (variant===2 && jumpy>.4)) ? size*.6 : 0;
const s = size*(0.9+rnd()*0.2);
x.fillStyle = shirt; x.beginPath(); x.ellipse(px, y+s*1.25-up, s*.72, s*.65, 0, Math.PI, 0); x.fill(); x.fillRect(px-s*.72, y+s*1.25-up, s*1.44, s*.6);
x.fillStyle = skin; x.beginPath(); x.arc(px, y+s*.55-up, s*.42, 0, Math.PI*2); x.fill();
if(cap){ x.fillStyle = shirt===C.navy?C.or:C.navy; x.beginPath(); x.arc(px, y+s*.45-up, s*.44, Math.PI, 0); x.fill(); }
if(up){ x.strokeStyle = skin; x.lineWidth = s*.22; x.lineCap="round"; x.beginPath(); x.moveTo(px-s*.6, y+s-up); x.lineTo(px-s*.8, y-up*.6); x.moveTo(px+s*.6, y+s-up); x.lineTo(px+s*.8, y-up*.6); x.stroke(); }
}
}
crowdX.filter = blur ? `blur(${blur}px)` : "none"; crowdX.drawImage(L, 0, 0, W, 75); crowdX.filter = "none";
};
deck(12, 5, 3.2, 4.3, 0.55);
deck(46, 5, 4.6, 6.0, 0.25, (px,r) => r<3 && px>212 && px<268);
x.filter = "none";
return c;
}
const CROWD = [makeCrowd(0), makeCrowd(1), makeCrowd(2)];
let hype = 0;
function drawCrowd(now){
g.drawImage(CROWD[0], 0, 0, W, 75);
if(hype > 0.02){ g.globalAlpha = Math.min(1, hype*1.4); g.drawImage(CROWD[1 + (Math.floor(now/140)%2)], 0, 0, W, 75); g.globalAlpha = 1; }
const n = hype > .1 ? 7 : (Math.random()<.06 ? 1 : 0);
for(let i=0;i<n;i++){ const fx = Math.random()*W, fy = 12 + Math.random()*55;
const fl = g.createRadialGradient(fx,fy,0,fx,fy,5); fl.addColorStop(0,"rgba(255,255,255,.95)"); fl.addColorStop(1,"rgba(255,255,255,0)");
g.fillStyle = fl; g.fillRect(fx-5,fy-5,10,10); }
}
/* ---------- static background, drawn once at 3x ---------- */
const bg = document.createElement("canvas"); bg.width = W*SC; bg.height = H*SC;
(function(){
const b = bg.getContext("2d"); b.scale(SC,SC);
let seed = 5; const rnd = () => (seed = (seed*16807)%2147483647)/2147483647;
let gr = b.createLinearGradient(0,0,0,44); gr.addColorStop(0,"#03072A"); gr.addColorStop(1,"#14226A"); b.fillStyle = gr; b.fillRect(0,0,W,46);
// decks
gr = b.createLinearGradient(0,10,0,38); gr.addColorStop(0,"#161E57"); gr.addColorStop(1,"#222C70"); b.fillStyle = gr; b.fillRect(0,10,W,28);
b.fillStyle = C.navy; b.fillRect(0,37,W,8); b.fillStyle = C.or; b.fillRect(0,37,W,1.2); b.fillRect(0,44,W,.8);
b.font = "bold 5.5px Arial"; b.fillStyle = "#B8C2E6"; b.textAlign = "center";
for(let x=60;x<W;x+=120) b.fillText("MARIETTA BASEBALL ACADEMY", x, 42.8);
gr = b.createLinearGradient(0,45,0,73); gr.addColorStop(0,"#1A2361"); gr.addColorStop(1,"#27327A"); b.fillStyle = gr; b.fillRect(0,45,W,28);
b.fillStyle = "#0A1242"; b.fillRect(213,45,56,17);
// backstop wall, padded panels, ad boards
gr = b.createLinearGradient(0,72,0,89); gr.addColorStop(0,"#13206E"); gr.addColorStop(1,"#08124A"); b.fillStyle = gr; b.fillRect(0,72,W,17);
for(let x=0;x<W;x+=24){ const pg = b.createLinearGradient(x,73,x+22,88); pg.addColorStop(0,"#1B2A80"); pg.addColorStop(1,"#0C175A"); b.fillStyle = pg; b.beginPath(); b.roundRect(x+1,73,22,15,2); b.fill(); }
b.fillStyle = C.or; b.fillRect(0,72,W,1.3); b.fillStyle = "#040828"; b.fillRect(0,88,W,1.5);
const board = (x,w,bgc,fg,txt,size) => { const bgd = b.createLinearGradient(x,75,x,86); bgd.addColorStop(0,shade(bgc,.15)); bgd.addColorStop(1,shade(bgc,-.12));
b.fillStyle = bgd; b.beginPath(); b.roundRect(x,75,w,11,1.5); b.fill(); b.fillStyle = fg; b.font = `bold ${size}px Arial`; b.textAlign="center"; b.fillText(txt,x+w/2,83.2); };
board(30,110,C.or,C.navy,"MARIETTA BASEBALL ACADEMY",7);
board(185,110,C.chalk,C.navy,"PITCH SELECTION GUIDE",7.4);
board(340,110,C.or,C.navy,"mbaseballacademy.com",7.3);
// dugouts
[[0,70],[410,70]].forEach(([x,w]) => { const dg = b.createLinearGradient(0,82,0,96); dg.addColorStop(0,"#03061E"); dg.addColorStop(1,"#0C1340"); b.fillStyle = dg; b.fillRect(x,82,w,14);
b.fillStyle = C.navy3; b.fillRect(x,80,w,3); b.fillStyle = C.or; b.fillRect(x,80,w,.9);
for(let i=0;i<6;i++){ b.fillStyle = "#23307A"; b.beginPath(); b.roundRect(x+6+i*11,87,6,9,2); b.fill(); b.fillStyle = "#D59C77"; b.beginPath(); b.arc(x+9+i*11,85.5,1.8,0,Math.PI*2); b.fill(); b.fillStyle=C.navy; b.beginPath(); b.arc(x+9+i*11,84.9,1.9,Math.PI,0); b.fill(); } });
// grass bands + mowed wedges + texture
let y = 89, band = 5, k = 0;
while(y < H){ b.fillStyle = k%2 ? C.g1 : C.g2; b.fillRect(0,y,W,band+1); y += band; band = Math.min(band*1.18+.6, 36); k++; }
b.save(); b.beginPath(); b.rect(0,89,W,H); b.clip();
for(let i=-9;i<=9;i+=2){ const a1 = Math.PI/2 + i*0.11, a2 = a1 + 0.11;
b.fillStyle = "rgba(0,40,10,.12)"; b.beginPath(); b.moveTo(240,110); b.lineTo(240+Math.cos(a1)*700,110+Math.sin(a1)*700); b.lineTo(240+Math.cos(a2)*700,110+Math.sin(a2)*700); b.closePath(); b.fill(); }
for(let i=0;i<5000;i++){ const gx = rnd()*W, gy = 89 + Math.pow(rnd(),0.8)*(H-89); b.fillStyle = rnd()<.5 ? "rgba(0,40,0,.18)" : "rgba(160,230,140,.10)"; b.fillRect(gx,gy,.5+ (gy-89)/180,.5+(gy-89)/140); }
b.restore();
const dirtTex = (clipFn, n) => { b.save(); b.beginPath(); clipFn(); b.clip(); for(let i=0;i<n;i++){ b.fillStyle = rnd()<.5 ? "rgba(90,50,20,.22)" : "rgba(255,220,170,.18)"; b.fillRect(rnd()*W, 89+rnd()*(H-89), .7, .7); } b.restore(); };
// baseline cutouts
b.fillStyle = C.dirt;
const cutL = () => { b.moveTo(214,120); b.lineTo(0,258); b.lineTo(0,236); b.lineTo(206,116); b.closePath(); };
const cutR = () => { b.moveTo(266,120); b.lineTo(W,258); b.lineTo(W,236); b.lineTo(274,116); b.closePath(); };
b.beginPath(); cutL(); b.fill(); b.beginPath(); cutR(); b.fill();
// home plate dirt with texture and cleat marks
gr = b.createRadialGradient(240,117,10,240,117,100); gr.addColorStop(0,C.dirtL); gr.addColorStop(1,C.dirt);
b.fillStyle = gr; b.beginPath(); b.ellipse(240,117,94,26,0,0,Math.PI*2); b.fill();
b.fillStyle = "rgba(120,70,30,.4)"; b.beginPath(); b.ellipse(240,122,74,14,0,0,Math.PI*2); b.fill();
dirtTex(() => { b.ellipse(240,117,94,26,0,0,Math.PI*2); cutL(); cutR(); }, 5500);
b.fillStyle = "rgba(90,50,20,.35)"; for(const [cx,cy] of [[200,125],[280,125],[240,108]]) for(let i=0;i<14;i++){ b.beginPath(); b.ellipse(cx+(rnd()-.5)*18, cy+(rnd()-.5)*6, 1.4, .6, 0, 0, Math.PI*2); b.fill(); }
// on-deck circles
[[108,104],[372,104]].forEach(([x,yy]) => { b.fillStyle = C.dirtL; b.beginPath(); b.ellipse(x,yy,15,4,0,0,Math.PI*2); b.fill(); b.fillStyle = C.or; b.beginPath(); b.ellipse(x,yy,9,2.2,0,0,Math.PI*2); b.fill(); b.fillStyle = C.navy; b.font="bold 2.4px Arial"; b.textAlign="center"; b.fillText("MBA",x,yy+.8); });
// chalk
b.strokeStyle = "rgba(246,244,236,.95)"; b.lineWidth = 1.5;
b.beginPath(); b.moveTo(179,137); b.lineTo(-20,259); b.moveTo(301,137); b.lineTo(500,259); b.stroke(); // centered in the dirt base paths
b.lineWidth = .9; b.strokeStyle = "rgba(246,244,236,.85)";
const box = (x0,x1) => { b.beginPath(); b.moveTo(x0-1.2,111.5); b.lineTo(x1+1.2,111.5); b.lineTo(x1+2.6,136.5); b.lineTo(x0-2.6,136.5); b.closePath(); b.stroke(); };
b.lineWidth = 1.1; box(174,225); box(255,306);
b.beginPath(); b.moveTo(223.8,111.5); b.lineTo(226,100.5); b.lineTo(254,100.5); b.lineTo(256.2,111.5); b.stroke(); // catcher's box
// plate
b.fillStyle = "#FFFFFF"; b.beginPath(); b.moveTo(231,121); b.lineTo(249,121); b.lineTo(249,124); b.lineTo(240,127); b.lineTo(231,124); b.closePath(); b.fill();
b.fillStyle = "#C3C4CC"; b.fillRect(231,123.3,18,.8);
// mound
gr = b.createRadialGradient(168,262,20,168,280,170); gr.addColorStop(0,C.dirtL); gr.addColorStop(1,C.dirt);
b.fillStyle = gr; b.beginPath(); b.ellipse(168,286,160,48,0,0,Math.PI*2); b.fill();
dirtTex(() => b.ellipse(168,286,160,48,0,0,Math.PI*2), 3500);
b.fillStyle = "#FFFFFF"; b.fillRect(132,253,52,4.6); b.fillStyle = "#C9C9CF"; b.fillRect(132,256.6,52,1);
b.fillStyle = "#F2F2F2"; b.beginPath(); b.ellipse(236,262,6,3,0,0,Math.PI*2); b.fill(); b.fillStyle="rgba(0,0,0,.2)"; b.beginPath(); b.ellipse(237,264,6,1.5,0,0,Math.PI*2); b.fill();
// light towers + beams
const tower = (x) => {
const glow = b.createRadialGradient(x+16,6,2,x+16,6,80); glow.addColorStop(0,"rgba(255,246,215,.6)"); glow.addColorStop(1,"rgba(255,246,215,0)");
b.fillStyle = glow; b.fillRect(x-70,-70,180,170);
const beam = b.createLinearGradient(x+16,6,240,120); beam.addColorStop(0,"rgba(255,246,215,.10)"); beam.addColorStop(1,"rgba(255,246,215,0)");
b.fillStyle = beam; b.beginPath(); b.moveTo(x+6,10); b.lineTo(x+26,10); b.lineTo(x<240?330:230,H); b.lineTo(x<240?150:40,H); b.closePath(); b.fill();
b.fillStyle = "#3A4470"; b.beginPath(); b.roundRect(x,-4,32,16,2); b.fill();
for(let i=0;i<5;i++) for(let j=0;j<3;j++){ b.fillStyle = "#FFF7DC"; b.beginPath(); b.roundRect(x+2+i*6,-2+j*5,4,3,1); b.fill(); }
};
tower(18); tower(430);
// stars
for(let i=0;i<40;i++){ b.fillStyle = `rgba(255,255,255,${.3+rnd()*.5})`; b.fillRect(60+rnd()*360, rnd()*9, .6, .6); }
// light sheen across the infield grass
const sh = b.createRadialGradient(240,170,10,240,170,200); sh.addColorStop(0,"rgba(255,255,230,.10)"); sh.addColorStop(1,"rgba(255,255,230,0)");
b.fillStyle = sh; b.fillRect(0,89,W,H-89);
// vignette
const vg = b.createRadialGradient(240,140,110,240,140,340); vg.addColorStop(0,"rgba(0,0,0,0)"); vg.addColorStop(1,"rgba(0,5,30,.5)");
b.fillStyle = vg; b.fillRect(0,0,W,H);
})();
/* backstop netting sits in front of the lower deck */
const net = document.createElement("canvas"); net.width = W*SC; net.height = 75*SC;
(function(){ const n = net.getContext("2d"); n.scale(SC,SC); n.strokeStyle = "rgba(220,228,255,.10)"; n.lineWidth = .35;
for(let x=150;x<=330;x+=2.4){ n.beginPath(); n.moveTo(x,46); n.lineTo(x,73); n.stroke(); }
for(let y=46;y<=73;y+=2.4){ n.beginPath(); n.moveTo(150,y); n.lineTo(330,y); n.stroke(); }
n.strokeStyle = "rgba(200,210,240,.5)"; n.lineWidth = .8; n.beginPath(); n.moveTo(150,46); n.lineTo(150,73); n.moveTo(330,46); n.lineTo(330,73); n.moveTo(150,46); n.lineTo(330,46); n.stroke(); })();
/* ---------- ballpark scoreboard: balls, strikes, outs for the situation ---------- */
function drawScoreboard(it){
const x=192, y=0, w=96, h=12.5;
g.fillStyle = C.out; g.fillRect(x-1.5,y,w+3,h+1.5); g.fillStyle = C.or; g.fillRect(x-.8,y,w+1.6,h+.8);
const sg = g.createLinearGradient(0,y,0,y+h); sg.addColorStop(0,"#0A1455"); sg.addColorStop(1,"#030824"); g.fillStyle = sg; g.fillRect(x,y,w,h);
let bl=-1, st=-1, ou=-1;
if(it){ const m = /^(\d)-(\d)$/.exec(it.count); if(m){ bl=+m[1]; st=+m[2]; } const o = /^(\d)$/.exec(String(it.outs)); if(o) ou=+o[1]; }
const group = (lab, x0, n, val, on) => {
g.fillStyle = "#AEB8DD"; g.font = "bold 5px Arial"; g.textAlign = "left"; g.fillText(lab, x0, y+8.3);
for(let i=0;i<n;i++){ const cx = x0+7+i*5.6, lit = val>i;
if(lit){ const gl = g.createRadialGradient(cx,y+6.4,0,cx,y+6.4,4); gl.addColorStop(0,on); gl.addColorStop(1,"rgba(0,0,0,0)"); g.fillStyle = gl; g.fillRect(cx-4,y+2.4,8,8); }
g.fillStyle = lit ? on : (val<0 ? "#1A2150" : "#222A5E"); g.beginPath(); g.arc(cx, y+6.4, 1.9, 0, Math.PI*2); g.fill(); }
};
group("B", x+4, 3, bl, "#5BE08C"); group("S", x+34, 2, st, "#FF5A4E"); group("O", x+60, 2, ou, "#FF914D");
if(it && bl<0){ g.fillStyle = "#6F7AA8"; g.font = "bold 3.6px Arial"; g.textAlign="right"; g.fillText("ANY", x+w-3, y+4.8); }
}
/* ---------- mini diamond inset (drawn at screen scale, outside the camera) ---------- */
function drawInset(bases){
const x=398, y=192, w=72, h=66;
g.fillStyle = "rgba(0,0,0,.35)"; g.beginPath(); g.roundRect(x-1,y+1,w+6,h+6,4); g.fill();
g.fillStyle = C.or; g.beginPath(); g.roundRect(x-3,y-3,w+6,h+6,4); g.fill();
g.fillStyle = C.navy; g.beginPath(); g.roundRect(x-1.5,y-1.5,w+3,h+3,3); g.fill();
g.save(); g.beginPath(); g.roundRect(x,y,w,h,2); g.clip();
g.fillStyle = C.g1; g.fillRect(x,y,w,h); for(let i=0;i<w;i+=8){ g.fillStyle = C.g2; g.fillRect(x+i,y,4,h); }
const cx=x+w/2, cy=y+h/2+4, r=24;
g.fillStyle = C.dirt; g.beginPath(); g.moveTo(cx,cy+r); g.lineTo(cx+r,cy); g.lineTo(cx,cy-r); g.lineTo(cx-r,cy); g.closePath(); g.fill();
g.fillStyle = C.g2; g.beginPath(); g.moveTo(cx,cy+r-9); g.lineTo(cx+r-9,cy); g.lineTo(cx,cy-r+9); g.lineTo(cx-r+9,cy); g.closePath(); g.fill();
g.fillStyle = C.dirtL; g.beginPath(); g.arc(cx,cy,3,0,Math.PI*2); g.fill();
const P = {1:[cx+r,cy],2:[cx,cy-r],3:[cx-r,cy]};
[1,2,3].forEach(k => {
const on = bases && bases.indexOf(k)>-1;
g.save(); g.translate(P[k][0],P[k][1]); g.rotate(Math.PI/4); g.fillStyle = C.out; g.fillRect(-4.3,-4.3,8.6,8.6); g.fillStyle = on?C.or:C.white; g.fillRect(-3.4,-3.4,6.8,6.8); g.restore();
if(on) drawParts([Lb([P[k][0]-1.2,P[k][1]-6],[P[k][0]-1.4,P[k][1]-2],1,1,C.gray), Lb([P[k][0]+1.2,P[k][1]-6],[P[k][0]+1.6,P[k][1]-2],1,1,C.gray), El(P[k][0],P[k][1]-8.5,2.4,3,C.gray), El(P[k][0],P[k][1]-12.5,2.2,2.2,C.red)]);
});
g.fillStyle = C.white; g.beginPath(); g.moveTo(cx-3,cy+r-3); g.lineTo(cx+3,cy+r-3); g.lineTo(cx+3,cy+r); g.lineTo(cx,cy+r+2); g.lineTo(cx-3,cy+r); g.closePath(); g.fill();
g.restore();
g.font = "bold 6.5px Arial"; g.textAlign = "center"; g.fillStyle = C.chalk;
g.fillText(bases==null ? "ANY RUNNERS" : bases.length ? "RUNNERS ON" : "BASES EMPTY", cx, y+9);
}
/* ---------- zone mapping: pitcher's view, inside to a righty is screen right ---------- */
const ZX = 240, ZY = 98, ZCX = 5.6, ZCY = 7;
function zonePos(r,c){ return [ZX + (c-2)*ZCX, ZY + (r-2)*ZCY]; } // left-handed hitter: inside (col 0) is screen left
const toScreen = (x,y) => [x,y];
/* ---------- particles ---------- */
const PARTS = [];
function puff(x,y,n,col,spd){ for(let i=0;i<n;i++){ const a=Math.random()*6.28, v=(.4+Math.random())*spd; PARTS.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v-.3,life:1,col,s:.8+Math.random()*1.4}); } }
let ring = null; // expanding impact ring
function drawParticles(){
for(let i=PARTS.length-1;i>=0;i--){ const p=PARTS[i]; p.x+=p.vx; p.y+=p.vy; p.vy+=.045; p.life-=.03;
if(p.life<=0){ PARTS.splice(i,1); continue; }
g.globalAlpha = p.life; g.fillStyle = p.col; g.beginPath(); g.arc(p.x,p.y,p.s,0,Math.PI*2); g.fill(); }
g.globalAlpha = 1;
if(ring){ ring.t += .06; if(ring.t>=1) ring=null; else { g.strokeStyle = `rgba(255,255,255,${1-ring.t})`; g.lineWidth = 1.2*(1-ring.t)+.3; g.beginPath(); g.arc(ring.x,ring.y,2+ring.t*14,0,Math.PI*2); g.stroke(); } }
}
/* ------------------------------------------------ animation */
const IDLE = { pf:0, swingT:0, bunt:0, mitt:[246,99], ump:0, ball:null, hit:null, trail:[], cam:1 };
let scene = Object.assign({}, IDLE, {trail:[]});
const FOCUS = [240,100];
function drawBall(b){
g.fillStyle = "rgba(0,0,0,.3)"; g.beginPath(); g.ellipse(b.sx,b.sy,b.s*1.1,b.s*.38,0,0,Math.PI*2); g.fill();
g.fillStyle = ballGrad(b.x,b.y,b.s,"#F4F4F6"); g.beginPath(); g.arc(b.x,b.y,b.s,0,Math.PI*2); g.fill();
g.strokeStyle = "rgba(20,20,40,.55)"; g.lineWidth = .35; g.stroke();
const sp = performance.now()*0.035; g.strokeStyle = "#D0342C"; g.lineWidth = Math.max(.35,b.s*.14);
g.beginPath(); g.arc(b.x-b.s*.55*Math.cos(sp),b.y,b.s*.75,-1,1); g.stroke();
g.beginPath(); g.arc(b.x+b.s*.55*Math.cos(sp),b.y,b.s*.75,Math.PI-1,Math.PI+1); g.stroke();
}
function render(now){
now = now || performance.now();
const z = scene.cam;
g.setTransform(SC*z,0,0,SC*z,SC*FOCUS[0]*(1-z),SC*FOCUS[1]*(1-z));
g.drawImage(bg,0,0,W,H);
drawCrowd(now);
drawScoreboard(current());
g.drawImage(net,0,0,W,75);
drawUmp(scene.ump);
drawCatcher(scene.mitt[0], scene.mitt[1]);
g.save(); g.translate(480,0); g.scale(-1,1); drawBatter(scene.swingT, now); g.restore(); // left-handed hitter: mirrored into the other box
// motion streak behind the ball
if(scene.trail.length > 1){ const a = scene.trail[0], b = scene.trail[scene.trail.length-1];
const sg = g.createLinearGradient(a[0],a[1],b[0],b[1]); sg.addColorStop(0,"rgba(255,255,255,0)"); sg.addColorStop(1,"rgba(255,255,255,.55)");
g.strokeStyle = sg; g.lineCap = "round"; g.lineWidth = b[2]*1.5; g.beginPath(); g.moveTo(a[0],a[1]); g.lineTo(b[0],b[1]); g.stroke(); }
if(scene.ball) drawBall(scene.ball);
drawPitcher(scene.pf, now);
if(scene.hit) drawBall(scene.hit);
drawParticles();
g.setTransform(SC,0,0,SC,0,0);
drawInset(current() ? current().bases : null);
if(hype > 0) hype = Math.max(0, hype - 0.007);
}
/* Only redraws while the game is on screen: the loop stops when it's scrolled
away or its tab is hidden, and restarts when it comes back. */
let ctpOn = true, ctpRaf = 0;
function loop(now){ ctpRaf = 0; if(!ctpOn) return; render(now); ctpRaf = requestAnimationFrame(loop); }
ctpRaf = requestAnimationFrame(loop);
if("IntersectionObserver" in window){
new IntersectionObserver(function(es){
ctpOn = es[es.length-1].isIntersecting;
if(ctpOn){ if(!ctpRaf) ctpRaf = requestAnimationFrame(loop); }
}).observe(document.getElementById("mba-ctp"));
}
function tween(dur, fn){ return new Promise(res => { const t0 = performance.now();
const step = now => { const t = Math.min(1,(now-t0)/dur); fn(t); if(t<1) requestAnimationFrame(step); else res(); };
requestAnimationFrame(step); }); }
const wait = ms => new Promise(r => setTimeout(r, ms));
function camTo(z, dur){ const z0 = scene.cam; return tween(dur, t => { scene.cam = lerp(z0, z, ease(t)); }); }
async function windup(){
await tween(260, t => { scene.pf = t; });
await wait(90);
await tween(210, t => { scene.pf = 1 + t; });
await tween(90, t => { scene.pf = 2 + t; });
SFX.whoosh();
}
/* radar gun readings by age group and pitch type */
function radar(kind){
const base = S.age==="o" ? {fb:76, ch:66, bb:64} : {fb:61, ch:51, bb:50};
return (base[kind]||base.fb) + Math.floor(Math.random()*4) - 1;
}
function showRadar(kind){
const el = document.getElementById("radar"); if(!el) return;
el.innerHTML = `${radar(kind)} <small>MPH</small><span>${ {fb:"FASTBALL",ch:"CHANGEUP",bb:"BREAKING BALL"}[kind] }</span>`;
el.classList.remove("on"); void el.offsetWidth; el.classList.add("on");
}
/* throw a pitch at (tx,ty). kind: fb | ch | bb. resolves when the ball reaches the plate. */
async function pitch(tx,ty,kind){
const sx=163, sy=108;
const dur = kind==="fb"?430 : kind==="ch"?680 : 590;
scene.trail = [];
const follow = tween(360, t => { scene.pf = 3 + t; });
const zoom = camTo(1.45, dur*.95);
await tween(dur, t => {
const e = kind==="fb" ? t : ease(t);
let x = sx + (tx-sx)*e, y = sy + (ty-sy)*e - Math.sin(Math.PI*e)*14;
if(kind==="bb"){ x += Math.sin(Math.PI*e)*7*(1-e); y += Math.pow(e,5)*6; }
if(kind==="ch"){ y += Math.pow(e,3)*3; }
const s = 4.6 - 3.1*e;
scene.trail.push([x,y,s]); if(scene.trail.length>7) scene.trail.shift();
scene.ball = {x,y,s, sx:x+4*(1-e), sy: lerp(250,126,e)};
});
await Promise.all([follow, zoom]);
scene.trail = [];
showRadar(kind);
}
function swing(dur){ return tween(dur, t => { scene.swingT = t; }); }
function flash(txt, cls){
const f = document.getElementById("flash"); f.textContent = txt; f.className = "flash " + cls;
void f.offsetWidth; f.classList.add("go");
}
function clearFlash(){ const f=document.getElementById("flash"); f.className="flash"; f.textContent=""; const r=document.getElementById("radar"); if(r) r.classList.remove("on"); }
function shake(){ const s=document.getElementById("screen"); s.classList.remove("shake"); void s.offsetWidth; s.classList.add("shake"); }
/* ---------- outcomes: what happens on screen and the matching verdict text ---------- */
const OUTCOME = {
3: { right:"take", flash:"STRIKE ONE!", verdict:"RIGHT CALL. HE TOOK IT FOR STRIKE ONE." },
35: { right:"buntPop", flash:"POPPED UP!", verdict:"RIGHT CALL. HE POPPED THE BUNT UP. EASY OUT.",
wrong:"buntDown", wflash:"BUNT DOWN!", wverdict:"WRONG CALL. HE GOT THE BUNT DOWN AND MOVED THE RUNNER UP." },
36: { right:"buntMiss", flash:"MISSED IT!", verdict:"RIGHT CALL. HE COULDN'T GET THE BUNT DOWN. THE RUNNER IS HUNG OUT AT HOME.",
wrong:"buntDown", wflash:"SQUEEZE SCORES!", wverdict:"WRONG CALL. HE GOT THE SQUEEZE DOWN AND THE RUN SCORED." },
37: { verdict:"RIGHT CALL. HE SWUNG THROUGH IT AND YOUR CATCHER HAS A CLEAN THROW." },
38: { verdict:"RIGHT CALL. FASTBALL AWAY GAVE YOUR CATCHER A CLEAN THROWING LANE." },
39: { right:"pick", flash:"RUNNER HELD!", verdict:"RIGHT CALL. YOU LOOKED HIM BACK AND THE RUNNER HELD." },
40: { right:"iw", flash:"BALL FOUR", verdict:"RIGHT CALL. BALL FOUR. HE TAKES FIRST ON YOUR TERMS." }
};
/* batter squares around to bunt: faces the pitcher, bat level in front of the plate */
const BUNT_POSE = {open:1, len:29, head:[262,69],shF:[256,81],shB:[270,81],elF:[252,88],elB:[268,89],hands:[258,88],hipF:[260,100],hipB:[271,100],knF:[257,114],ftF:[258,128],knB:[273,113],ftB:[274,126],bat:-Math.PI+0.05};
function squareUp(){ return tween(260, t => { scene.bunt = ease(t); }); }
async function playStrike(it, opt){
const oc = OUTCOME[it.n] || {}, type = oc.right || "swing";
if(type==="pick"){
await tween(260, t => { scene.pf = t*.6; }); SFX.whoosh();
await tween(260, t => { scene.pf = .6 - t*.6; });
hype = 1; flash(oc.flash,"k"); SFX.cheer(); return oc.verdict;
}
const cell = (it.tgt && it.tgt.length) ? it.tgt[Math.floor(Math.random()*it.tgt.length)] : [2,2];
const [tx,ty] = zonePos(cell[0],cell[1]);
const m0 = scene.mitt.slice();
await tween(260, t => { scene.mitt = [lerp(m0[0],tx,ease(t)), lerp(m0[1],ty,ease(t))]; });
if(type==="iw"){
await windup(); await pitch(tx,ty,"fb"); scene.ball=null; SFX.pop(); puff(tx,ty,8,"#E9C99A",.7);
flash(oc.flash,"k"); camTo(1,500); return oc.verdict;
}
if(type==="buntPop" || type==="buntMiss"){
const sq = squareUp(); await windup(); await sq;
await pitch(tx,ty,opt.kind);
if(type==="buntMiss"){
scene.ball = null; SFX.pop(); puff(tx,ty,12,"#E9C99A",.9); ring = {x:tx,y:ty,t:0};
await tween(300, t => { scene.bunt = 1 - t*.15; });
hype = 1; flash(oc.flash,"k"); SFX.cheer(); return oc.verdict;
}
// pop-up: soft contact, ball floats straight up and drops toward the catcher
SFX.pop(); scene.ball = null; puff(tx-6,ty,8,"#FFFFFF",.8); camTo(1.2,400);
await tween(1100, t => { const y = ty - 70*Math.sin(Math.PI*t) + 6*t; scene.hit = {x:tx-6+8*t, y, s:1.6, sx:tx-6+8*t, sy:ty+14}; });
scene.hit = null; SFX.pop(); scene.mitt = [tx+2, ty+4];
hype = 1; flash(oc.flash,"k"); SFX.cheer(); return oc.verdict;
}
await windup();
const kind = opt.kind;
const flight = pitch(tx,ty,kind);
if(type==="take"){
await flight; // he never offers
} else {
await wait(kind==="fb"?170:kind==="ch"?190:260); // he commits early: fooled
await Promise.all([flight, swing(kind==="fb"?300:240)]);
}
scene.ball = null; SFX.pop(); puff(tx,ty,14,"#E9C99A",.9); ring = {x:tx,y:ty,t:0};
await tween(360, t => { scene.ump = t; });
SFX.strike(); hype = 1;
flash(oc.flash || "STEE-RIKE!","k"); SFX.cheer();
return oc.verdict || "RIGHT CALL. HE SWUNG RIGHT THROUGH IT.";
}
async function playHit(it, opt){
const oc = OUTCOME[it.n] || {}, type = oc.wrong || "hit";
const [tx,ty] = zonePos(opt.cell[0],opt.cell[1]);
const m0 = scene.mitt.slice();
await tween(220, t => { scene.mitt = [lerp(m0[0],tx,ease(t)), lerp(m0[1],ty,ease(t))]; });
if(type==="buntDown"){
const sq = squareUp(); await windup(); await sq;
await pitch(tx,ty,opt.kind);
scene.ball = null; SFX.pop(); puff(tx-4,ty,10,"#FFFFFF",.8); camTo(1,500);
// dead bunt: drops in front of the plate and rolls up the third-base line toward the camera
await tween(1200, t => {
const bx = lerp(tx-4, 330, t), groundY = lerp(122, 190, t), hop = Math.abs(Math.sin(t*Math.PI*3))*(8*(1-t));
scene.hit = {x:bx, y:groundY-hop, s:1.6+2.4*t, sx:bx, sy:groundY+1};
});
scene.hit = null; hype = .8;
flash(oc.wflash,"h"); SFX.groan(); return oc.wverdict;
}
await windup();
const flight = pitch(tx,ty,opt.kind);
await wait(opt.kind==="fb"?260:opt.kind==="ch"?440:370);
await Promise.all([flight, swing(170)]);
scene.ball = null; SFX.crack(); shake(); puff(tx,ty,18,"#FFFFFF",1.4); ring = {x:tx,y:ty,t:0}; hype = .8;
const dir = Math.random()<.5?-1:1, gap = 110 + Math.random()*120;
camTo(1, 500);
await tween(1000, t => {
const x = tx + dir*gap*t*1.5, yAir = ty - 120*Math.sin(Math.PI*t*.8) + 60*t;
scene.hit = {x, y:yAir, s:1.6+13*t*t, sx:x, sy:ty+26+150*t};
});
scene.hit = null;
const calls = ["BASE HIT!","DOUBLE!","IN THE GAP!","BASE HIT!"];
flash(calls[Math.floor(Math.random()*calls.length)],"h"); SFX.groan();
return "WRONG CALL. HE HAMMERED IT.";
}
/* ---------- UI helpers ---------- */
const $ = id => document.getElementById(id);
const narrow = () => window.innerWidth <= 760;
function into(el, block){ try{ el.scrollIntoView({behavior:"smooth", block:block||"nearest"}); }catch(e){} }
function pips(){
const res = S.res || [];
$("pips").innerHTML = S.qs.map((_,j) => `<i class="${res[j]===true?"k":res[j]===false?"h":j===S.i&&S.phase!=="end"?"cur":""}"></i>`).join("");
$("numTag").textContent = `PITCH ${Math.min(S.i+1,S.qs.length)} OF ${S.qs.length}`;
}
function prompt(html){ $("prompt").innerHTML = html; }
/* ------------------------------------------------ quiz */
function current(){ return S.phase==="title"||S.phase==="end" ? null : S.qs[S.i]; }
function shuffle(a){ a=a.slice(); for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; }
function kindOf(text){ return /^break|^BB/i.test(text) ? "bb" : /^change|^CH/i.test(text) ? "ch" : "fb"; }
function cellOf(loc){
const l = loc.toLowerCase();
if(/dirt|below/.test(l)) return [3,2];
if(/up and in/.test(l)) return [1,1];
if(/middle-in|inside|in\b/.test(l)) return [2,1];
if(/up/.test(l)) return [1,2];
if(/away|outside|off|corner|edge/.test(l)) return [2,3];
if(/down|knees/.test(l)) return [3,2];
return [2,2];
}
function buildOptions(it){
const call = S.age==="o" && it.call14 ? it.call14 : it.call;
const right = { txt: expand(call), loc: it.loc, correct:true, kind: kindOf(call) };
const wrong = (WRONG[it.n]||[]).map(w => ({ txt:w[0], loc:w[1], correct:false, kind:kindOf(w[0]), cell:cellOf(w[1]) }));
if(S.age==="y") wrong.forEach(w => { if(w.kind==="bb"){ w.txt="Changeup"; w.kind="ch"; } }); // no breaking balls at 11-13U
return shuffle([right].concat(wrong));
}
function hud(){
const it = current();
const L = document.getElementById("hudL"), Rr = document.getElementById("hudR");
if(it){ L.innerHTML = `SIT <b>#${it.n}</b><br>COUNT <b>${it.count.toUpperCase()}</b><br>OUTS <b>${String(it.outs).toUpperCase()}</b>`; }
Rr.innerHTML = `K'S <b>${S.k}</b><br>STREAK <b>${S.streak}</b><br>PITCH <b>${Math.min(S.i+1,S.qs.length)}/${S.qs.length}</b>`;
}
function runners(b){ return b==null ? "Any" : b.length ? b.map(x=>["","1st","2nd","3rd"][x]).join(" & ") : "Bases empty"; }
function showQuestion(){
S.phase="ready"; scene = Object.assign({}, IDLE, {mitt:[246,99], trail:[]}); clearFlash(); render(); hud();
const it = current();
document.getElementById("secTag").textContent = `${it.sec.id} · ${it.sec.name.toUpperCase()}`;
pips();
prompt(S.i===0 ? `WHAT'S THE CALL?<span class="hint">Tap the pitch and spot you'd call.</span>` : "WHAT'S THE CALL?");
document.getElementById("qTitle").textContent = it.title;
document.getElementById("facts").innerHTML =
`<div class="fact"><small>COUNT</small>${it.count}</div><div class="fact"><small>RUNNERS</small>${runners(it.bases)}</div><div class="fact"><small>OUTS</small>${it.outs}</div>`;
const box = document.getElementById("opts"); box.innerHTML = ""; box.classList.remove("locked");
S.opts = buildOptions(it);
S.opts.forEach((o,k) => {
const b = document.createElement("button"); b.className="opt"; b.id = "opt"+k;
b.innerHTML = `<span class="key">${k+1}</span><span>${o.txt}<span class="loc">${o.loc}</span></span>`;
b.addEventListener("click", () => answer(k));
box.appendChild(b);
});
$("result").hidden = true; $("result").classList.remove("show");
}
async function answer(k){
if(S.phase!=="ready") return;
S.phase="anim"; SFX.blip();
const it = current(), o = S.opts[k];
$("opts").classList.add("locked");
$("opts").querySelectorAll(".opt").forEach((b,j) => { b.disabled = true; b.classList.add(j===k ? (o.correct?"right":"wrong") : (S.opts[j].correct?"right":"dim")); });
prompt("HERE COMES THE PITCH…");
if(narrow()) into($("screen")); /* make sure the pitch is on screen */
let said;
(S.res = S.res || [])[S.i] = !!o.correct;
if(o.correct){ S.k++; S.streak++; S.best=Math.max(S.best,S.streak); said = await playStrike(it,o); }
else { S.streak=0; S.missed.push(it); said = await playHit(it,o); }
hud(); pips();
$("opts").querySelectorAll(".opt").forEach((b,j) => {
const lab = j===k ? (o.correct ? "YOUR CALL ✓" : "YOUR CALL") : (S.opts[j].correct ? "RIGHT CALL" : "");
if(lab) b.insertAdjacentHTML("beforeend", `<span class="tag">${lab}</span>`);
});
prompt(o.correct ? `<span style="color:var(--k)">NICE CALL.</span>` : `<span style="color:var(--hit)">NOT THIS TIME.</span>`);
const call = S.age==="o" && it.call14 ? it.call14 : it.call;
const v = document.getElementById("verdict");
v.textContent = said;
v.className = "verdict " + (o.correct?"k":"h");
document.getElementById("rCall").textContent = `The call: ${expand(call)} · ${it.loc}`;
document.getElementById("rWhy").textContent = it.why;
document.getElementById("rCue").innerHTML = `<small>CATCHER'S CUE</small>“${it.cue}"`;
document.getElementById("result").hidden = false; $("result").classList.add("show");
document.getElementById("nextBtn").textContent = S.i+1 >= S.qs.length ? "SEE YOUR SCORE" : "NEXT PITCH";
document.getElementById("nextBtn").focus({preventScroll:true});
/* The explanation and NEXT button sit right on (or right under) the field, so only nudge if they're off screen. */
setTimeout(() => into($("result")), 60);
S.phase="result";
}
function next(){
if(S.phase!=="result") return;
S.i++;
if(S.i >= S.qs.length) return endGame();
showQuestion();
into($("panel"), narrow() ? "start" : "nearest"); /* back up to the new situation */
}
function endGame(){
S.phase="end"; hud();
const pct = S.k / S.qs.length;
const grade = pct>=1?"ACE": pct>=.66?"STARTER": pct>=.33?"SETUP MAN":"BACK TO THE BULLPEN";
document.getElementById("grade").textContent = grade;
document.getElementById("endLine").textContent = `${S.k} of ${S.qs.length} right calls · best streak ${S.best}`;
document.getElementById("misses").innerHTML = S.missed.length
? S.missed.map(it => `<div><b>#${it.n}</b> ${it.title}: <b>${expand(S.age==="o"&&it.call14?it.call14:it.call)}</b>, ${it.loc.toLowerCase()}</div>`).join("")
: `<div style="border-left-color:var(--k)">Perfect game. Every call was right.</div>`;
$("result").hidden = true; pips();
document.getElementById("end").hidden = false;
document.getElementById("subBtn").focus({preventScroll:true});
document.getElementById("screen").classList.add("ended");
setTimeout(() => into($("end"), narrow() ? "start" : "nearest"), 60);
track("ctp_trial_complete", {score:S.k});
}
function start(){
ac(); if(AC && AC.state==="suspended") AC.resume();
document.getElementById("screen").classList.remove("ended");
const order = [7,12,35]; /* same 3 situations, same order, every time */
S.qs = order.map(n => ALL.find(it => it.n===n)).filter(Boolean).slice(0, S.len); S.i=0; S.k=0; S.streak=0; S.best=0; S.missed=[]; S.res=[];
document.getElementById("start").hidden = true; document.getElementById("end").hidden = true;
showQuestion();
}
/* ------------------------------------------------ wiring */
function seg(id, key, attr, onPick){
const el = document.getElementById(id);
el.addEventListener("click", e => { const b = e.target.closest("button"); if(!b) return;
if(onPick(b.getAttribute(attr)) === false) return;
el.querySelectorAll("button").forEach(x => x.classList.toggle("on", x===b)); });
}
seg("ageSeg","age","data-age", v => { S.age=v; try{ localStorage.setItem("ctp_age",v); }catch(e){} });
document.querySelectorAll("#ageSeg button").forEach(b => b.classList.toggle("on", b.getAttribute("data-age")===S.age));
document.getElementById("playBtn").addEventListener("click", start);
document.getElementById("againBtn").addEventListener("click", start);
document.getElementById("nextBtn").addEventListener("click", next);
function track(ev, extra){ try{ (window.dataLayer = window.dataLayer || []).push(Object.assign({event:ev}, extra||{})); }catch(e){} }
document.getElementById("subBtn").addEventListener("click", e => {
track("ctp_trial_subscribe_click");
const href = e.currentTarget.getAttribute("href") || "";
if(href.charAt(0) !== "#") return; /* full URL: normal navigation */
const id = href.slice(1);
const target = id && document.getElementById(id);
if(!target) return; /* no anchor found: let the browser handle the #link */
e.preventDefault();
target.scrollIntoView({behavior:"smooth", block:"start"});
try{ history.replaceState(null, "", href); }catch(err){}
});
document.getElementById("mute").addEventListener("click", e => { S.sound=!S.sound; e.currentTarget.textContent = "SOUND " + (S.sound?"ON":"OFF"); e.currentTarget.setAttribute("aria-pressed", String(!S.sound)); });
document.addEventListener("keydown", e => {
if(e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
if(S.phase==="ready" && ["1","2","3"].includes(e.key)) answer(+e.key-1);
else if(S.phase==="result" && e.key==="Enter"){ e.preventDefault(); next(); }
});
/* Sound starts on the first tap (browsers block it until then). */
document.addEventListener("pointerdown", function(){ const a = ac(); if(a && a.state === "suspended") a.resume(); });
/* No intro screen — the trial starts straight on the first pitch. */
start();
}
/* Build the game only once its tab is open and it's near the screen.
Drawing the ballpark and crowd is the heaviest work on the page, so
visitors who never scroll to it never pay for it. */
(function(){
var box = document.getElementById("mba-ctp");
if(!box){ return; }
if(!("IntersectionObserver" in window)){ mbaCtpInit(); return; }
var io = new IntersectionObserver(function(es){
if(es[es.length-1].isIntersecting){ io.disconnect(); mbaCtpInit(); }
}, {rootMargin:"400px"});
io.observe(box);
})();
(function () {
var y = document.getElementById('footer-year');
if (y) y.textContent = new Date().getFullYear();
})();
(function () {
// Trust section logo: reuse the header logo image already on the page.
var src = document.querySelector('.brand-chip img');
var dst = document.getElementById('trust-logo');
if (src && dst) dst.src = src.src; else if (dst) dst.parentNode.style.display = 'none';
})();
(function () {
// A real slice of the app's weakness taxonomy (Category > Weakness >
// Sub-Cause) with drills actually mapped to each sub-cause, so the demo
// works exactly like the Weakness-Based planner in the app.
var DEMO = [{"c":"Hitting","weak_total":10,"weaks":[{"w":"Weak contact","total":8,"subs":[{"s":"Hitting off the front foot too early (weight transfer issue)","d":[{"n":"Golfer Drill","p":"Hitting","why":"The golf-swing-into-load transition specifically trains a smooth, controlled weight shift — the direct fix for rushing onto the front foot early."},{"n":"Step Back Drill","p":"Hitting","why":"Drill's entire purpose is preventing an early lunge forward, training controlled weight transfer timing."}]},{"s":"Casting hands away from the body","d":[{"n":"Hands In Front Drill","p":"Hitting","why":"Drill isolates and reinforces a direct, efficient hand path from load to contact, the direct fix for hands drifting away from the body."},{"n":"Connection Ball Drill","p":"Hitting","why":"Ball wedged between bat and body gives immediate physical feedback the moment the hands cast away — about as direct a fix as exists for this cause."}]},{"s":"Not using the lower half/hips in the swing (arm-only swing)","d":[{"n":"Top Hand Drill","p":"Hitting","why":"Drill explicitly cues hip rotation synced with the top hand, correcting an arms-only swing."},{"n":"Hands In Front Drill","p":"Hitting","why":"Tim-verified: Hands in Front Drill directly trains this sub-cause."}]},{"s":"Poor barrel control — hitting off the end or handle","d":[{"n":"Double Tee Drill","p":"Hitting","why":"Tim-verified: Double Tee Drill directly trains this sub-cause."},{"n":"Inside/Outside Tee Drill","p":"Hitting","why":"Tim-verified: Inside/Outside Tee Drill directly trains this sub-cause."}]}]},{"w":"Too many strikeouts","total":10,"subs":[{"s":"Chasing pitches out of the strike zone","d":[{"n":"Two Ball Toss","p":"Hitting","why":"Tim-verified: Two Ball Toss directly trains this sub-cause."}]},{"s":"Bat speed too slow to catch up to velocity","d":[{"n":"Ball Drop Drill","p":"Hitting","why":"Tim-verified: Ball Drop Drill directly trains this sub-cause."}]},{"s":"Long/looping swing — takes too long to get to the ball","d":[{"n":"Double Tee Drill","p":"Hitting","why":"Requiring the first ball to strike the second demands a short, direct, level swing path — a looping swing will miss the back ball entirely."},{"n":"Connection Ball Drill","p":"Hitting","why":"Tim-verified: Connection Ball Drill directly trains this sub-cause."}]},{"s":"Too passive — freezing on hittable strikes early in the count","d":[{"n":"Two Ball Toss","p":"Hitting","why":"Tim-verified: Two Ball Toss directly trains this sub-cause."},{"n":"Zombies Drill","p":"Hitting","why":"Tim-verified: Zombies Drill directly trains this sub-cause."}]}]},{"w":"Trouble with high velocity","total":9,"subs":[{"s":"Slow hand load/trigger — not ready in time","d":[{"n":"Ball Drop Drill","p":"Hitting","why":"Tim-verified: Ball Drop Drill directly trains this sub-cause."}]},{"s":"Long swing path, can't get started early enough","d":[{"n":"Connection Ball Drill","p":"Hitting","why":"Tim-verified: Connection Ball Drill directly trains this sub-cause."},{"n":"Bottom Hand Drill","p":"Hitting","why":"Tim-verified: Bottom Hand Drill directly trains this sub-cause."}]},{"s":"Stepping in the bucket (bailing out) against hard throwers","d":[{"n":"Bucket Drill","p":"Hitting","why":"Near-exact match to the sub-cause itself — the bucket behind the front foot physically blocks the batter from stepping out/bailing."}]},{"s":"No consistent timing mechanism (load/rhythm)","d":[{"n":"Golfer Drill","p":"Hitting","why":"Tim-verified: Golfer Drill directly trains this sub-cause."},{"n":"Blind Tee Drill","p":"Hitting","why":"Tim-verified: Blind Tee Drill directly trains this sub-cause."}]}]}]},{"c":"Fielding","weak_total":18,"weaks":[{"w":"Throws sailing high or wide","total":9,"subs":[{"s":"Poor arm slot/release point consistency","d":[{"n":"Throwing Accuracy","p":"Infield","why":"Repeated throws at a fixed low-trajectory target from a set distance directly trains a consistent release point and arm slot for accuracy."},{"n":"Target Throw Drill","p":"Outfield","why":"Repeated throws at a fixed target from center field trains a consistent release point and arm slot for accuracy over distance."}]},{"s":"Rushing the throw instead of setting the feet","d":[{"n":"5 Ball Scramble Drill","p":"Shortstop","why":"Drill explicitly cues setting the feet for each throw despite the emphasis on speed — training against rushing even under time pressure."},{"n":"Three Ball Throw Drill","p":"Third Base","why":"Drill explicitly demands both speed and accuracy on rapid, successive throws — training against the accuracy loss that comes from rushing."}]},{"s":"Poor footwork/direction to the target","d":[{"n":"Bucket Target Drill","p":"Second Base","why":"Shuffle-step approach into a one-step throw at a fixed target directly trains footwork and direction toward the target."},{"n":"Infield Circuit Drill","p":"Infield","why":"Drill explicitly coaches stepping in the direction of each throw around the horn, directly training the footwork that keeps throws on line."}]},{"s":"Not squaring shoulders to the target","d":[{"n":"Box Drill","p":"Infield","why":"Drill explicitly coaches catching heel-to-palm with the chest squared up before each throw, reinforcing the squared-up base that keeps throws on line."}]}]},{"w":"Booting easy ground balls","total":9,"subs":[{"s":"Not getting in front of the ball (poor angle/positioning)","d":[{"n":"Goalie Drill","p":"Shortstop","why":"Elimination format built entirely around staying in front of and securing the ball within a confined area."}]},{"s":"Glove not down and ready in fielding position","d":[{"n":"Glove Drill","p":"Infield","why":"Drill isolates the ready position — glove low between the legs — before ever introducing movement, building the fundamental glove-ready habit."},{"n":"Walking Picks Challenge","p":"First Base","why":"Continuous low-stance picking on the move reinforces keeping the glove down and ready throughout, rather than only right before contact."}]},{"s":"Stopping the feet before the ball arrives instead of playing through it","d":[{"n":"Slow Roller Drill","p":"Third Base","why":"Drill explicitly requires charging aggressively and fielding on the move rather than settling into a breakdown position — the direct opposite of stopping the feet early."},{"n":"Box Ground Ball Drill","p":"Infield","why":"Fast, continuous ground balls rolled hard test whether players keep moving through the ball under a quick rotation instead of settling and stopping their feet."}]},{"s":"Poor footwork getting into position","d":[{"n":"Fungo Picks","p":"First Base","why":"Varying direction and hop type forces the fielder to consistently adjust footwork to get in proper position for each ball."},{"n":"Figure 8 Drill","p":"Second Base","why":"Figure-eight movement pattern forces continual footwork adjustment before each field, on both forehand and backhand sides."}]}]},{"w":"Trouble receiving throws at a base","total":7,"subs":[{"s":"Poor footwork stretching or picking a throw at the bag","d":[{"n":"1st Base Footwork","p":"First Base","why":"Drill is built entirely around footwork for receiving throws at the bag, a direct match for the new category."},{"n":"Third Base Footwork","p":"Third Base","why":"Drill is built entirely around footwork for receiving throws at the bag, a direct match for the new category."}]},{"s":"Late reaction to a high or wide throw","d":[{"n":"Overhead Throw Drill","p":"First Base","why":"Drill isolates handling a high throw safely at the bag, a direct match for the new category."},{"n":"Knock Down Drill","p":"First Base","why":"Blocking and recovering an errant throw at the bag is a variation on handling a bad throw safely."}]},{"s":"Difficulty applying a tag while also receiving the throw","d":[{"n":"Pick-Off Drill","p":"First Base","why":"Drill combines receiving a pickoff throw with applying the tag, the exact combination this sub-cause describes."},{"n":"2B Pick-Off Drill","p":"Second Base","why":"Drill combines receiving a pickoff throw with applying the tag at the bag, the exact combination this sub-cause describes."}]}]}]},{"c":"Outfield","weak_total":3,"weaks":[{"w":"Trouble playing balls off the wall","total":6,"subs":[{"s":"Poor technique locating the wall with the off-hand","d":[{"n":"Fence Drill","p":"Outfield","why":"Drill's explicit safety technique — extending an arm to feel the fence — is a direct match for the new category."}]},{"s":"Misjudging the rebound angle or distance","d":[{"n":"Off the Wall Drill","p":"Outfield","why":"Drill is built entirely around reading and playing a rebound off the outfield wall, a direct match for the new category."}]},{"s":"Losing track of the ball while finding the wall","d":[{"n":"Catch at the Wall Drill","p":"Outfield","why":"Drill combines the drop-step first move with locating the wall by feel while keeping eyes on the ball, a direct match for the new category."}]}]},{"w":"Struggling with deep or over-the-shoulder routes","total":6,"subs":[{"s":"Poor sprint mechanics tracking a ball over the shoulder","d":[{"n":"Over-the-Shoulder Chase Drill","p":"Infield","why":"Drill is genuinely outfield content (tracking and catching a deep fly ball over the shoulder), now a direct match now that Outfield is its own category."},{"n":"Over-The-Shoulder Catch","p":"Outfield","why":"Drill is built entirely around tracking and catching a ball over the shoulder while sprinting, a direct match for the new category."}]}]},{"w":"Not backing up bases or plays","total":5,"subs":[{"s":"Slow to react and get moving toward the backup spot","d":[{"n":"Back Up First Base Drill","p":"Outfield","why":"Drill is built entirely around a fast, correct reaction to back up a potential overthrow at first, a direct match for the new category."},{"n":"Back Up Third Base Drill","p":"Outfield","why":"Drill is built entirely around a fast, correct reaction to back up a potential overthrow at third, a direct match for the new category."}]}]}]},{"c":"Pitching","weak_total":9,"weaks":[{"w":"Walking too many batters","total":8,"subs":[{"s":"Inconsistent arm slot/release point","d":[{"n":"Kneeling Pitching Drill","p":"Pitching","why":"Removing the lower half isolates the arm action from load to follow-through, so the pitcher can groove a consistent slot and release without the rest of the delivery masking flaws."},{"n":"Wall Drill","p":"Pitching","why":"Drill exists specifically to catch and correct arm wrapping (the arm hitting the wall), a common cause of an inconsistent slot and release point."}]},{"s":"Rushing the delivery, losing balance","d":[{"n":"Balanced Delivery Drill","p":"Pitching","why":"Drill isolates the glide into the power position and requires it to stay controlled and balanced, directly correcting a delivery that gets ahead of itself."},{"n":"Static Leg Lift Drill","p":"Pitching","why":"Holding the leg lift for a count before continuing forces a controlled, unhurried transition into the pitching motion instead of rushing through the balance phase."}]},{"s":"Poor lower half mechanics/direction to the plate","d":[{"n":"Power Position Pitching Drill","p":"Pitching","why":"Drill emphasizes driving off the back leg and keeping shoulders/hips/chin aligned to the target from the power position, the lower-half direction that keeps pitches on line to the plate."},{"n":"Balance Drill","p":"Pitching","why":"Drill isolates the rocker step, pivot-foot positioning, and weight distribution — the exact lower-half fundamentals that keep the delivery directed at the plate."}]}]},{"w":"Slow reaction on comebackers/PFP","total":4,"subs":[{"s":"Poor fielding-ready position after delivery","d":[{"n":"Bunt Defense Drill","p":"Pitching","why":"Drill trains the same quick transition from pitching finish into an athletic fielding position that PFP requires, though the actual play (bunt coverage, not a comebacker) isn't represented in the weakness system — same…"}]},{"s":"Slow reaction/reflexes off the bat","d":[{"n":"Glove Flip to First Base","p":"Pitching","why":"Drill has the pitcher finish the delivery in an athletic fielding position and react to a live ground ball hit back at them, training the reflexes comebacker reaction depends on."}]},{"s":"Poor footwork covering first base on grounders","d":[{"n":"Covering First Base","p":"Pitching","why":"Drill is built entirely around the pitcher's footwork and route sprinting to cover first base after a ball is hit to the right side — a direct, named match."}]}]},{"w":"Struggling to get ahead in the count","total":6,"subs":[{"s":"Inconsistent fastball command","d":[{"n":"Knockout","p":"Pitching","why":"Elimination-format target work forces the pitcher to consistently hit a specific spot rather than just throw strikes, directly training the fastball command needed to get ahead early."},{"n":"L-Screen Drill","p":"Pitching","why":"Throwing through a narrow gap gives immediate pass/fail feedback on hitting a tight target, sharpening the command needed to get ahead early in counts."}]}]}]},{"c":"Catching","weak_total":11,"weaks":[{"w":"Weak/inaccurate throws down to bases","total":6,"subs":[{"s":"Poor footwork on the throw (not stepping toward the target)","d":[{"n":"Throwdown From Catcher","p":"First Base","why":"This is the catcher making a quick, accurate pickoff throw off the pitch — the same throwing skill trained here as when throwing down to second on a steal attempt, just aimed at first instead. Reclassified from a 1B drill to…"},{"n":"Field and Throw","p":"Catching","why":"Drill also requires accurate throws to first and third immediately off the bunt-fielding footwork, so it doubles as reps on setting the feet toward the target under time pressure."}]},{"s":"Rushing the throw instead of setting a strong base","d":[{"n":"Double Steal Drill","p":"Second Base","why":"One of the four coaching cues is explicitly the catcher's throw: “must be accurate and easy for the second baseman to handle." Same throwing skill as a normal steal throw, just cut off deliberately partway."}]},{"s":"Poor arm slot/release consistency","d":[{"n":"Throw Down Accuracy","p":"Catching","why":"The same drill's emphasis on a consistent low-trajectory release also builds a repeatable arm slot for throws down to bases generally."}]}]},{"w":"Trouble blocking balls in the dirt","total":5,"subs":[{"s":"Poor footwork getting the body in front of the ball","d":[{"n":"Slide and Block","p":"Catching","why":"Drill has the catcher slide on the leg guards toward a ball thrown into the dirt, directly training the footwork needed to get the body in front of blocks."},{"n":"Line Drill","p":"Catching","why":"Repeated blocking reps with a snap-back to the center line train the footwork to get in front of balls in the dirt under fatigue."}]},{"s":"Hesitating instead of committing to the block","d":[{"n":"Goalie Drill","p":"Catching","why":"Competitive scoring format rewards aggressive, committed blocking rather than hesitating, directly targeting that habit."}]}]},{"w":"Slow pop time on throws to second","total":6,"subs":[{"s":"Slow exchange from glove to throwing hand","d":[{"n":"Glove to Hand Transfer","p":"Catching","why":"Drill is built entirely around speeding up the glove-to-hand exchange, the first link in pop time."}]},{"s":"Poor footwork out of the crouch","d":[{"n":"Agility Ladder Hip Turns","p":"Catching","why":"Repeated hip-turn-into-throwing-position reps build the quick footwork transition out of the crouch that a fast pop time relies on."}]},{"s":"Rising too slowly out of the stance","d":[{"n":"Up/Downs with Ball Toss","p":"Catching","why":"The explosive down-and-up sequence directly trains getting out of the crouch quickly, the same movement pop time depends on."}]},{"s":"Inefficient throwing mechanics/release","d":[{"n":"Throw Down Accuracy","p":"Catching","why":"Drill trains the full transition-to-throw sequence with a low-trajectory accuracy target, reinforcing efficient release mechanics for throws to second."}]}]}]}];
var catSel = document.getElementById('demo-cat');
var weakSel = document.getElementById('demo-weak');
var subSel = document.getElementById('demo-sub');
var out = document.getElementById('demo-result');
if (!catSel) return;
function esc(s) {
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
function fill(sel, items, placeholder) {
sel.innerHTML = '<option value="">' + placeholder + '</option>' +
items.map(function (t, i) { return '<option value="' + i + '">' + esc(t) + '</option>'; }).join('');
}
function reset(sel, placeholder) {
sel.innerHTML = '<option value="">' + placeholder + '</option>';
sel.disabled = true;
}
function hint(text) { out.innerHTML = '<p class="demo-hint">' + text + '</p>'; }
fill(catSel, DEMO.map(function (c) { return c.c; }), '— Select a category —');
catSel.insertAdjacentHTML('beforeend', '<option value="" disabled>Base Running (in the full app)</option>');
catSel.addEventListener('change', function () {
reset(subSel, '— Select a weakness first —');
if (catSel.value === '') { reset(weakSel, '— Select a category first —'); hint('Start by picking a category.'); return; }
var cat = DEMO[+catSel.value];
fill(weakSel, cat.weaks.map(function (w) { return w.w; }), '— Select a weakness —');
weakSel.disabled = false;
hint('Now pick the weakness your team is having. (The app has all ' + cat.weak_total + ' ' + esc(cat.c.toLowerCase()) + ' weaknesses — a few are shown here.)');
});
weakSel.addEventListener('change', function () {
if (weakSel.value === '') { reset(subSel, '— Select a weakness first —'); hint('Now pick the weakness your team is having.'); return; }
var w = DEMO[+catSel.value].weaks[+weakSel.value];
fill(subSel, w.subs.map(function (s) { return s.s; }), '— Select a sub-cause —');
subSel.disabled = false;
hint('Last step: what’s actually causing it? (The app lists all ' + w.total + ' sub-causes for this weakness.)');
});
subSel.addEventListener('change', function () {
if (subSel.value === '') { hint('Last step: what’s actually causing it?'); return; }
var sub = DEMO[+catSel.value].weaks[+weakSel.value].subs[+subSel.value];
out.innerHTML =
'<div class="demo-result-head"><h4>Suggested drills</h4><span>Matched to: ' + esc(sub.s) + '</span></div>' +
'<div class="demo-drills">' + sub.d.map(function (d) {
return '<div class="result-card"><div class="drill-name">' + esc(d.n) + '</div>' +
'<span class="drill-tag">' + esc(d.p) + '</span>' +
'<p class="why"><b>Why it fits:</b> ' + esc(d.why) + '</p></div>';
}).join('') + '</div>' +
'<p class="demo-addnote">In the app, tap <b>Add to Plan</b> to drop any of these straight into a practice.</p>';
});
})();
// Drill counts and the "new drills this week" banner are filled in by
// WordPress when the page loads (the mba_drill_count and
// mba_new_drills_banner shortcodes, from the "MBA Toolkit — live drill
// count shortcodes" WPCode snippet).
(function () {
// The old mock quiz engine (hardcoded example questions) lived here.
// It's replaced by the four real trial games above, embedded via their
// WordPress shortcodes — this script switches which one is visible
// when a tab is clicked, AND supports linking straight to one game
// from an ad: each embed has its own id (#trial-defense,
// #trial-baserunning, #trial-hitting, #trial-pitching), so a link like
// https://mbaseballacademy.com/toolkit/#trial-hitting lands on that tab
// already selected and scrolled into view.
var tabBar = document.getElementById('quiz-tabs');
if (!tabBar) return;
var tabs = Array.prototype.slice.call(tabBar.querySelectorAll('.tab-btn'));
var panels = Array.prototype.slice.call(document.querySelectorAll('.trial-game-embed'));
function activate(key) {
tabs.forEach(function (b) { b.classList.toggle('active', b.dataset.key === key); });
panels.forEach(function (p) { p.classList.toggle('is-active', p.dataset.key === key); });
}
tabs.forEach(function (btn) {
btn.addEventListener('click', function () {
var key = btn.dataset.key;
activate(key);
// Keep the address bar in sync (so the coach can copy the link
// straight out of the browser too) without spamming browser
// history with one entry per tab click.
var hash = '#trial-' + key;
if (window.history && window.history.replaceState) {
window.history.replaceState(null, '', hash);
} else {
window.location.hash = hash;
}
});
});
// Land on the right tab if the page was opened with a #trial-<key>
// link (e.g. from an ad pointed at a specific game).
function activateFromHash(scrollTo) {
var hash = window.location.hash.replace('#', '');
var match = /^trial-(defense|baserunning|hitting|pitching)$/.exec(hash);
if (!match) return false;
activate(match[1]);
if (scrollTo) {
var target = document.getElementById(hash);
if (target) target.scrollIntoView({ block: 'start' });
}
return true;
}
activateFromHash(true);
window.addEventListener('hashchange', function () { activateFromHash(true); });
// "Play the Quiz Game" on each guide card jumps to that guide's trial
// game above/below. If the link's hash is already the current one,
// the browser fires no hashchange, so handle the click directly too.
document.querySelectorAll('.pill-link[data-trial]').forEach(function (a) {
a.addEventListener('click', function (e) {
e.preventDefault();
var key = a.getAttribute('data-trial');
activate(key);
var hash = '#trial-' + key;
if (window.history && window.history.replaceState) window.history.replaceState(null, '', hash);
var target = document.getElementById('trial-' + key);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
})();
// Sample page viewer: click a guide page to see it bigger, arrows to flip
// through that guide's 3 pages, Esc / click outside / X to close.
(function () {
var viewer = document.getElementById('page-viewer');
if (!viewer) return;
var img = viewer.querySelector('img');
var cap = viewer.querySelector('figcaption');
var set = [], idx = 0, lastFocus = null;
function show(i) {
idx = (i + set.length) % set.length;
img.src = set[idx].src;
img.alt = set[idx].alt;
var alt = set[idx].alt, k = alt.toLowerCase().indexOf('sample page:');
// No backslashes here on purpose (WordPress strips them from Custom HTML).
cap.textContent = (k === -1 ? alt : alt.slice(k + 12).trim()) + ' · ' + (idx + 1) + ' of ' + set.length;
}
function open(group, i) {
set = Array.prototype.slice.call(group.querySelectorAll('img'));
lastFocus = document.activeElement;
show(i);
viewer.classList.add('is-open');
document.body.style.overflow = 'hidden';
viewer.querySelector('.pv-close').focus();
}
function close() {
viewer.classList.remove('is-open');
document.body.style.overflow = '';
img.removeAttribute('src');
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
document.querySelectorAll('.guide-pages').forEach(function (group) {
var imgs = group.querySelectorAll('img');
imgs.forEach(function (im, i) {
im.setAttribute('tabindex', '0');
im.setAttribute('role', 'button');
im.addEventListener('click', function () { open(group, i); });
im.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(group, i); } });
});
});
viewer.querySelector('.pv-close').addEventListener('click', close);
viewer.querySelector('.pv-prev').addEventListener('click', function (e) { e.stopPropagation(); show(idx - 1); });
viewer.querySelector('.pv-next').addEventListener('click', function (e) { e.stopPropagation(); show(idx + 1); });
viewer.addEventListener('click', function (e) { if (e.target === viewer || e.target.tagName === 'FIGURE') close(); });
viewer.addEventListener('contextmenu', function (e) { if (e.target === img) e.preventDefault(); });
document.addEventListener('keydown', function (e) {
if (!viewer.classList.contains('is-open')) return;
if (e.key === 'Escape') close();
else if (e.key === 'ArrowLeft') show(idx - 1);
else if (e.key === 'ArrowRight') show(idx + 1);
});
// Swipe left/right on phones
var x0 = null;
viewer.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true });
viewer.addEventListener('touchend', function (e) {
if (x0 === null) return;
var dx = e.changedTouches[0].clientX - x0; x0 = null;
if (Math.abs(dx) > 40) show(idx + (dx < 0 ? 1 : -1));
});
})();
// ---- Video (VSL): muted preview while on screen; sound restarts it from the top ----
(function () {
var v = document.getElementById('mba-vsl');
var btn = document.getElementById('mba-vsl-sound');
if (!v || !btn) return;
var withSound = false;
var sent = {};
function track(name) {
if (sent[name]) return;
sent[name] = true;
if (window.fbq) fbq('trackCustom', name);
}
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if ('IntersectionObserver' in window && !reduce) {
new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (withSound) { if (!en.isIntersecting && !v.paused) v.pause(); return; }
if (en.isIntersecting) { v.preload = 'auto'; var p = v.play(); if (p && p.catch) p.catch(function () {}); }
else if (!v.paused) v.pause();
});
}, { threshold: 0.35 }).observe(v);
}
btn.addEventListener('click', function () {
withSound = true;
btn.hidden = true;
v.loop = false;
v.muted = false;
v.controls = true;
try { v.currentTime = 0; } catch (e) {}
var p = v.play(); if (p && p.catch) p.catch(function () {});
track('VSLPlayWithSound');
});
v.addEventListener('timeupdate', function () {
if (!withSound || !v.duration) return;
var pct = v.currentTime / v.duration;
if (pct >= 0.5) track('VSLWatched50');
if (pct >= 0.95) track('VSLWatched95');
});
})();
document.querySelectorAll('a[href*="add-to-cart="]').forEach(function (a) {
a.addEventListener('click', function () {
if (window.fbq) fbq('trackCustom', 'ChoosePlan', { plan: a.textContent.trim() });
});
});
// Upgrade prices for coaches who already own a bundle (full prices for everyone else).
// No backslashes in this block: WordPress strips them from Custom HTML.
(function () {
var cards = document.querySelectorAll('.price-card[data-tier]');
if (!cards.length || !window.fetch) return;
// Per-bundle drill counts: hidden if the shortcode didn't run, filled in below.
var countEls = document.querySelectorAll('[data-bundle-drills]');
countEls.forEach(function (el) {
var n = el.querySelector('.n');
if (n && n.textContent.indexOf('[') === 0) { n.textContent = ''; el.style.visibility = 'hidden'; }
});
function money(n) { return '$' + (Math.round(n * 100) / 100).toString().replace(/[.]00?$/, ''); }
fetch('/wp-admin/admin-ajax.php?action=mba_bundle_status', { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d) return;
countEls.forEach(function (el) {
var c = d.drills && d.drills[el.getAttribute('data-bundle-drills')];
var n = el.querySelector('.n');
if (c && n) { n.textContent = Number(c).toLocaleString(); el.style.visibility = ''; }
});
if (!d.loggedIn) return;
var note = document.getElementById('upgrade-note');
if (d.full) {
cards.forEach(function (card) {
var btn = card.querySelector('.btn');
if (btn) { btn.textContent = 'Open the Toolkit'; btn.href = d.appUrl; btn.className = 'btn btn-ghost btn-block'; }
});
if (note) note.textContent = 'Your Toolkit subscription already includes everything here.';
return;
}
if (!d.own) return;
cards.forEach(function (card) {
var tier = Number(card.getAttribute('data-tier'));
var amount = card.querySelector('.amount');
var btn = card.querySelector('.btn');
var sub = card.querySelector('.per-month');
if (tier <= d.own) {
card.classList.add('is-owned');
if (amount) amount.innerHTML = '<span class="owned-tag">' + (tier === d.own ? 'You own this' : 'Included in yours') + '</span>';
if (btn) { btn.textContent = 'Open the Toolkit'; btn.href = d.appUrl; btn.className = 'btn btn-ghost btn-block'; }
return;
}
var up = d.upgrade && d.upgrade[tier];
if (up === undefined || !amount) return;
amount.innerHTML = money(up) + '<small> to upgrade</small><span class="was">' + money(d.prices[tier]) + '</span>';
if (sub) sub.textContent = 'You own ' + d.ownName + ', so you pay only the difference';
if (btn) btn.textContent = 'Upgrade to ' + card.querySelector('.plan-name').textContent;
});
if (note) note.textContent = 'You own the ' + d.ownName + ' bundle. Upgrades cost only the difference.';
})
.catch(function () {});
})();
var awdr_params = {"ajaxurl":"https://mbaseballacademy.com/wp-admin/admin-ajax.php","nonce":"a269b71dbb","enable_update_price_with_qty":"show_when_matched","refresh_order_review":"0","custom_target_simple_product":"","custom_target_variable_product":"","js_init_trigger":"","awdr_opacity_to_bulk_table":"","awdr_dynamic_bulk_table_status":"1","awdr_dynamic_bulk_table_off":"on","custom_simple_product_id_selector":"","custom_variable_product_id_selector":""};
//# sourceURL=awdr-main-js-extra
jQuery(document).ready(function(){
jQuery(".pb_img_thumbnail").on("click",function(){
var clikedElID = jQuery(this).attr("id");
jQuery("#pb_lightbox"+clikedElID).css("display","block");
});
jQuery(".pb_single_img_lightbox").on("click",function(){
jQuery(this).css("display","none");
});
jQuery(window).scroll();
});
(function($) {
/**
* Copyright 2012, Digital Fusion
* Licensed under the MIT license.
* http://teamdf.com/jquery-plugins/license/
*
* @author Sam Sehnert
* @desc A small plugin that checks whether elements are within
* the user visible viewport of a web browser.
* only accounts for vertical position, not horizontal.
*/
$.fn.visible = function(partial) {
var $t = $(this),
$w = $(window),
viewTop = $w.scrollTop(),
viewBottom = viewTop + $w.height(),
_top = $t.offset().top,
_bottom = _top + $t.height(),
compareTop = partial === true ? _bottom : _top,
compareBottom = partial === true ? _top : _bottom;
return ((compareBottom <= viewBottom) && (compareTop >= viewTop));
};
jQuery(document).on("click", 'a[href^="#"]', function (event) {
event.preventDefault();
var clickedLinkElAttr = jQuery(this ).attr("href");
if(typeof(jQuery(clickedLinkElAttr).offset() ) != "undefined"){
jQuery("html, body").animate({
scrollTop: jQuery(jQuery.attr(this, "href")).offset().top
}, 500);
}
});
})(jQuery);
if(typeof pluginOpsCheckElViewFrame != "function" ){
function pluginOpsCheckElViewFrame (el) {
if (typeof jQuery === "function" && el instanceof jQuery) {
el = el[0];
}
if( typeof(el.getBoundingClientRect) == "function" ){
var rect = el.getBoundingClientRect();
windowInnerHeight = window.innerHeight;
if(rect.height >= windowInnerHeight){
rect.height = windowInnerHeight - 50;
}
docClientHeight = document.documentElement.clientHeight;
if( rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (windowInnerHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth) ) {
return "InView";
}else{
return "NotInView";
}
}else{
return "Function didnt work";
}
}
}
jQuery(window).scroll();
//# sourceURL=pluginops-landingpage-scripts-inline-js-footer-js-after
(function() {
// Always start from a fresh object: a `||` fallback would keep a same-named DOM element alive.
window.wcAnalytics = {};
const wcAnalytics = window.wcAnalytics;
// Set the assets URL for webpack to find the split assets.
wcAnalytics.assets_url = 'https://mbaseballacademy.com/wp-content/plugins/jetpack/jetpack_vendor/automattic/woocommerce-analytics/src/../build/';
// Set the REST API tracking endpoint URL.
wcAnalytics.trackEndpoint = "https://mbaseballacademy.com/wp-json/woocommerce-analytics/v1/track";
// Set common properties for all events.
wcAnalytics.commonProps = {"ui":0,"blog_id":214105619,"store_id":"9e9485fd-e095-4895-b00c-6f417d1ee0ad","url":"https://mbaseballacademy.com","woo_version":"11.2.1","wp_version":"7.1.3","store_admin":0,"device":"desktop","store_currency":"USD","timezone":"-04:00","is_guest":1,"package_version":"0.18.0"};
// Set the event queue.
wcAnalytics.eventQueue = [];
// Features.
wcAnalytics.features = {
ch: false,
sessionTracking: false,
proxy: false,
};
wcAnalytics.breadcrumbs = ["toolkit"];
// Page context flags.
wcAnalytics.pages = {
isAccountPage: false,
isCart: false,
};
})();
// Listen for clicks on the entire document
document.addEventListener('click', function(event) {
console.log(event.target.dataset)
// Check if the clicked element has the 'track-click' class
if (event.target.classList.contains('trk-clk-ulbp')) {
// Get the unique ID from the data-id attribute
const elementTxt = event.target.dataset.btntxt;
const elementUrl = event.target.dataset.btnhref;
// Create a FormData object to send data
const formData = new FormData();
formData.append('clicked_element_text', elementTxt);
formData.append('clicked_element_url', elementUrl);
formData.append('clicked_element_psid', 37396);
//console.log(formData.get('clicked_element_psid'));
// Use the Fetch API to send the data to our PHP script
fetch(
"https://mbaseballacademy.com/wp-admin/admin-ajax.php?action=ulpb_rc_current_usr_click&POPB_CTA_Nonce=4143ebe46f"
, {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(data => {
console.log(data); // Log the response from PHP (e.g., "Success!")
})
.catch(error => {
console.error('Error tracking click:', error);
});
}
});