<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>One on One: Dr. J vs. Larry Bird — Three.js Tribute</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#000;font-family:"Courier New",monospace;color:#fff}
  canvas{display:block}
  #hud{position:fixed;top:10px;left:0;right:0;display:flex;justify-content:center;gap:18px;pointer-events:none;font-weight:bold;text-shadow:0 0 6px #000}
  .box{background:rgba(0,0,0,.65);border:2px solid #555;padding:6px 14px;font-size:20px;letter-spacing:1px}
  .box.has{border-color:#fff;box-shadow:0 0 10px #fff}
  .bird{color:#5ce07f}.drj{color:#ff6b6b}.clock{color:#ffcc33}
  #msg{position:fixed;top:72px;left:0;right:0;text-align:center;font-size:32px;font-weight:bold;color:#ffd84d;text-shadow:2px 2px 0 #000,0 0 14px #000;pointer-events:none;opacity:0;transition:opacity .2s}
  #help{position:fixed;bottom:10px;left:0;right:0;text-align:center;font-size:13px;color:#bbb;text-shadow:1px 1px 0 #000;pointer-events:none}
  .overlay{position:fixed;inset:0;background:rgba(0,0,0,.78);display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}
  .overlay h1{font-size:46px;margin:0 0 6px;color:#ffd84d;text-shadow:3px 3px 0 #a00}
  .overlay h2{font-size:18px;color:#bbb;margin:0 0 28px;font-weight:normal}
  .pick{display:flex;gap:30px;margin-bottom:22px}
  .card{cursor:pointer;border:3px solid #555;padding:18px 26px;background:#111;font-size:20px;font-weight:bold;transition:.15s;min-width:200px}
  .card:hover{transform:scale(1.05);border-color:#fff}
  .card small{display:block;font-size:12px;color:#aaa;margin-top:6px;font-weight:normal;line-height:1.5}
  .overlay p{color:#ccc;font-size:14px;max-width:600px;line-height:1.7}
  .btn{cursor:pointer;border:3px solid #ffd84d;background:#222;color:#ffd84d;padding:12px 30px;font-size:20px;font-weight:bold;margin-top:14px}
  .hidden{display:none!important}
</style>
</head>
<body>
<div id="hud">
  <div class="box bird" id="hudBird">BIRD 0</div>
  <div class="box clock" id="hudClock">SHOT 24</div>
  <div class="box drj" id="hudJ">DR. J 0</div>
</div>
<div id="msg"></div>
<div id="help">WASD / Arrows: move &nbsp;|&nbsp; SPACE: shoot / dunk (near rim) / jump to block &nbsp;|&nbsp; SHIFT or X: steal &nbsp;|&nbsp; M: mute &nbsp;|&nbsp; R: restart &nbsp;|&nbsp; First to 15</div>

<div id="menu" class="overlay">
  <h1>ONE ON ONE</h1>
  <h2>DR. J vs. LARRY BIRD — a Three.js tribute</h2>
  <div class="pick">
    <div class="card bird" data-idx="0">LARRY BIRD<small>#33 Celtics<br>Deadly outside shot<br>Rarely breaks glass</small></div>
    <div class="card drj" data-idx="1">DR. J<small>#6 Sixers<br>Quick, monster dunks<br>Backboards fear him</small></div>
  </div>
  <p>Pick your player. Dunks can shatter the backboard — then the janitor has to come out and sweep it up. He's not going to be happy about it.</p>
</div>

<div id="over" class="overlay hidden">
  <h1 id="overTitle">GAME OVER</h1>
  <h2 id="overSub"></h2>
  <div class="btn" id="againBtn">PLAY AGAIN (R)</div>
</div>

<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';

/* =========================================================
   CONSTANTS & HELPERS
   ========================================================= */
const G = -12;                         // arcade gravity
const WIN = 15;
const COURT = { halfW: 7.5, baseline: -14, top: 0.3 };
const RIM = new THREE.Vector3(0, 3.05, -12.4);
const BOARD_Z = -13.0;
const V3 = (x=0,y=0,z=0)=>new THREE.Vector3(x,y,z);
const rand = (a,b)=>a+Math.random()*(b-a);
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const pick = a=>a[Math.floor(Math.random()*a.length)];
function box(w,h,d,mat){ const m=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),mat); m.castShadow=true; m.receiveShadow=true; return m; }

/* =========================================================
   AUDIO — everything synthesized with Web Audio
   ========================================================= */
const A = { ctx:null, master:null, crowd:null, crowdFilter:null, muted:false, noise:null };
function initAudio(){
  if (A.ctx) return;
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  A.ctx = ctx;
  A.master = ctx.createGain(); A.master.gain.value = 0.8; A.master.connect(ctx.destination);
  const n = ctx.sampleRate*2, b = ctx.createBuffer(1,n,ctx.sampleRate), d = b.getChannelData(0);
  for (let i=0;i<n;i++) d[i]=Math.random()*2-1;
  A.noise = b;
  // ambient crowd murmur (looping filtered noise)
  const src = ctx.createBufferSource(); src.buffer=b; src.loop=true;
  const f = ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=500;
  const g = ctx.createGain(); g.gain.value=0.04;
  src.connect(f).connect(g).connect(A.master); src.start();
  A.crowd=g; A.crowdFilter=f;
}
const ok = ()=>A.ctx && !A.muted;
function env(g,t0,peak,dur){ g.gain.setValueAtTime(0.0001,t0); g.gain.linearRampToValueAtTime(peak,t0+0.006); g.gain.exponentialRampToValueAtTime(0.0001,t0+dur); }
function tone(type,f0,f1,dur,vol,t0){
  t0 = t0 ?? A.ctx.currentTime;
  const o=A.ctx.createOscillator(), g=A.ctx.createGain();
  o.type=type; o.frequency.setValueAtTime(f0,t0);
  if (f1) o.frequency.exponentialRampToValueAtTime(f1,t0+dur);
  env(g,t0,vol,dur); o.connect(g).connect(A.master); o.start(t0); o.stop(t0+dur+0.05);
}
function noise(filterType,freq,Q,dur,vol,t0){
  t0 = t0 ?? A.ctx.currentTime;
  const s=A.ctx.createBufferSource(); s.buffer=A.noise; s.loop=true;
  const f=A.ctx.createBiquadFilter(); f.type=filterType; f.frequency.value=freq; f.Q.value=Q;
  const g=A.ctx.createGain(); env(g,t0,vol,dur);
  s.connect(f).connect(g).connect(A.master); s.start(t0, rand(0,1.5)); s.stop(t0+dur+0.05);
}
const sfx = {
  bounce(v=0.4){ if(!ok())return; tone('sine',170,55,0.14,v); },
  swish(){ if(!ok())return; noise('bandpass',2500,1.2,0.3,0.5); },
  rim(v=0.5){ if(!ok())return; [410,1030,1650,2400].forEach((f,i)=>tone('sine',f,f*0.98,0.5-i*0.08,v*0.25)); noise('highpass',3000,1,0.05,v*0.4); },
  slam(){ if(!ok())return; tone('sine',90,40,0.3,0.9); this.rim(0.8); noise('lowpass',800,1,0.15,0.6); },
  thud(){ if(!ok())return; tone('sine',120,60,0.15,0.5); },
  glass(){
    if(!ok())return; const t=A.ctx.currentTime;
    noise('highpass',3500,0.7,0.9,0.9,t); noise('bandpass',6000,2,0.5,0.6,t); tone('sine',110,50,0.25,0.7,t);
    for(let i=0;i<28;i++) tone('sine',rand(2500,9500),rand(2000,8000),rand(0.08,0.35),0.18,t+rand(0,1.2));
    for(let i=0;i<12;i++) tone('triangle',rand(1500,4500),null,rand(0.1,0.3),0.12,t+rand(0.6,2.8));
  },
  tink(){ if(!ok())return; tone('sine',rand(3000,7000),null,0.12,0.07); },
  whistle(){
    if(!ok())return; const t=A.ctx.currentTime;
    const o=A.ctx.createOscillator(); o.type='square'; o.frequency.value=2300;
    const l=A.ctx.createOscillator(); l.frequency.value=28; const lg=A.ctx.createGain(); lg.gain.value=60; l.connect(lg).connect(o.frequency);
    const f=A.ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=4000;
    const g=A.ctx.createGain(); env(g,t,0.15,0.7);
    o.connect(f).connect(g).connect(A.master); o.start(t); l.start(t); o.stop(t+0.8); l.stop(t+0.8);
  },
  buzzer(){ if(!ok())return; tone('square',170,170,0.8,0.22); tone('square',173,173,0.8,0.22); },
  squeak(){ if(!ok())return; tone('sine',1400,2600,0.09,0.07); },
  cheer(){ if(!ok())return; const t=A.ctx.currentTime; noise('bandpass',1100,0.5,2.5,0.6,t); noise('bandpass',700,0.5,2.0,0.4,t+0.1); },
  boo(){ if(!ok())return; const t=A.ctx.currentTime; noise('lowpass',350,0.8,1.6,0.5,t); tone('sawtooth',110,95,1.4,0.08,t); },
  grumble(){
    if(!ok())return; const t=A.ctx.currentTime;
    const o=A.ctx.createOscillator(); o.type='sawtooth'; let tt=t;
    for(let i=0;i<10;i++){ o.frequency.setValueAtTime(rand(75,140),tt); tt+=rand(0.06,0.14); }
    const f=A.ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=500;
    const g=A.ctx.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.25,t+0.05);
    g.gain.setValueAtTime(0.25,tt-0.1); g.gain.exponentialRampToValueAtTime(0.0001,tt+0.1);
    o.connect(f).connect(g).connect(A.master); o.start(t); o.stop(tt+0.2);
  },
  sweep(){ if(!ok())return; noise('bandpass',900,0.6,0.35,0.12); },
  hammer(){ if(!ok())return; const t=A.ctx.currentTime; for(let i=0;i<4;i++){ tone('square',900,300,0.08,0.25,t+i*0.25); noise('highpass',2000,1,0.05,0.3,t+i*0.25); } },
};

/* =========================================================
   SCENE
   ========================================================= */
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x07070f);
scene.fog = new THREE.Fog(0x07070f, 28, 60);

const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 200);
camera.position.set(0, 8.5, 4.5);
camera.lookAt(0, 1.2, -9);

scene.add(new THREE.HemisphereLight(0xffffff, 0x222233, 0.8));
const sun = new THREE.DirectionalLight(0xffffff, 1.6);
sun.position.set(6, 16, -4); sun.castShadow = true;
sun.shadow.mapSize.set(2048,2048);
Object.assign(sun.shadow.camera, {left:-18,right:18,top:18,bottom:-18,near:1,far:50});
scene.add(sun);

// arena floor beyond the court
const outer = new THREE.Mesh(new THREE.PlaneGeometry(60,60), new THREE.MeshStandardMaterial({color:0x1b1b26, roughness:1}));
outer.rotation.x=-Math.PI/2; outer.position.set(0,-0.01,-8); outer.receiveShadow=true; scene.add(outer);

// ---- Court with canvas-painted lines ----
function makeCourtTexture(){
  const c=document.createElement('canvas'); c.width=c.height=1024; const x=c.getContext('2d'); const S=64;
  for(let i=0;i<1024;i+=24){ x.fillStyle=`hsl(${28+Math.random()*6},${55+Math.random()*10}%,${50+Math.random()*10}%)`; x.fillRect(i,0,24,1024); }
  x.strokeStyle='rgba(60,30,10,0.3)'; x.lineWidth=1;
  for(let i=0;i<1024;i+=24){ x.beginPath(); x.moveTo(i,0); x.lineTo(i,1024); x.stroke(); }
  const cx=v=>(v+8)*S, cy=v=>(16+v)*S;
  x.fillStyle='rgba(15,85,65,0.85)'; x.fillRect(cx(-2.45),cy(-14),4.9*S,5.8*S);
  x.strokeStyle='#f5f5f5'; x.lineWidth=5;
  x.strokeRect(cx(-7.5),cy(-14),15*S,14*S);
  x.strokeRect(cx(-2.45),cy(-14),4.9*S,5.8*S);
  x.beginPath(); x.arc(cx(0),cy(-8.2),1.8*S,0,Math.PI*2); x.stroke();
  x.beginPath(); x.arc(cx(0),cy(-12.4),6.75*S,0.209,Math.PI-0.209); x.stroke();
  x.beginPath(); x.moveTo(cx(6.6),cy(-14)); x.lineTo(cx(6.6),cy(-11)); x.moveTo(cx(-6.6),cy(-14)); x.lineTo(cx(-6.6),cy(-11)); x.stroke();
  x.beginPath(); x.arc(cx(0),cy(0),1.8*S,Math.PI,Math.PI*2); x.stroke();
  x.beginPath(); x.arc(cx(0),cy(-12.4),1.25*S,0,Math.PI); x.stroke();
  x.fillStyle='rgba(255,255,255,0.45)'; x.font='bold 64px Impact, sans-serif'; x.textAlign='center';
  x.fillText('ONE ON ONE',cx(0),cy(-3.4));
  const t=new THREE.CanvasTexture(c); t.anisotropy=8; t.colorSpace=THREE.SRGBColorSpace; return t;
}
const floor = new THREE.Mesh(new THREE.PlaneGeometry(16,16), new THREE.MeshStandardMaterial({map:makeCourtTexture(), roughness:0.55}));
floor.rotation.x=-Math.PI/2; floor.position.set(0,0,-8); floor.receiveShadow=true; scene.add(floor);

// ---- Hoop assembly ----
const metal = new THREE.MeshStandardMaterial({color:0x888c94, metalness:0.7, roughness:0.4});
const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.09,0.09,3.9,12), metal); pole.position.set(0,1.95,-14.5); pole.castShadow=true; scene.add(pole);
const poleArm = box(0.1,0.1,1.5,metal); poleArm.position.set(0,3.75,-13.75); scene.add(poleArm);
const glassMat = new THREE.MeshStandardMaterial({color:0xbfe6ff, transparent:true, opacity:0.4, roughness:0.05, metalness:0.1, side:THREE.DoubleSide});
const board = new THREE.Mesh(new THREE.PlaneGeometry(1.8,1.05), glassMat); board.position.set(0,3.425,BOARD_Z); scene.add(board);
{ // target square on the glass
  const sq = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.PlaneGeometry(0.59,0.45)), new THREE.LineBasicMaterial({color:0xffffff}));
  sq.position.set(0,-0.25,0.004); board.add(sq);
}
const frameMat = new THREE.MeshStandardMaterial({color:0xdddddd});
[[1.86,0.05,0.05,0,0.55],[1.86,0.05,0.05,0,-0.55],[0.05,1.12,0.05,0.9,0],[0.05,1.12,0.05,-0.9,0]].forEach(([w,h,d,x,y])=>{ const m=box(w,h,d,frameMat); m.position.set(x,3.425+y,BOARD_Z); scene.add(m); });
const rim = new THREE.Mesh(new THREE.TorusGeometry(0.23,0.018,10,40), new THREE.MeshStandardMaterial({color:0xff5a1f, metalness:0.4, roughness:0.4}));
rim.rotation.x=Math.PI/2; rim.position.copy(RIM); rim.castShadow=true; scene.add(rim);
const bracket = box(0.1,0.06,0.4,metal); bracket.position.set(0,RIM.y-0.02,RIM.z-0.4); scene.add(bracket);
{ // net
  const pts=[], segs=12, rows=4, rTop=0.23, rBot=0.13, h=0.42;
  for(let i=0;i<segs;i++){ const a0=i/segs*Math.PI*2, a1=(i+1)/segs*Math.PI*2;
    for(let r=0;r<rows;r++){ const f0=r/rows,f1=(r+1)/rows, R0=rTop+(rBot-rTop)*f0, R1=rTop+(rBot-rTop)*f1;
      pts.push(Math.cos(a0)*R0,-h*f0,Math.sin(a0)*R0, Math.cos(a1)*R1,-h*f1,Math.sin(a1)*R1);
      pts.push(Math.cos(a1)*R0,-h*f0,Math.sin(a1)*R0, Math.cos(a0)*R1,-h*f1,Math.sin(a0)*R1); } }
  const g=new THREE.BufferGeometry(); g.setAttribute('position',new THREE.Float32BufferAttribute(pts,3));
  const net=new THREE.LineSegments(g,new THREE.LineBasicMaterial({color:0xffffff})); net.position.copy(RIM); scene.add(net);
}
let boardIntact = true;

// ---- Crowd (instanced) + bleachers ----
const crowd = { base:[], phase:[], body:null, head:null, count:0 };
{
  const seats=[];
  for(let r=0;r<6;r++){ const z=-15.8-r*0.9, y=0.55+r*0.6; for(let x=-10;x<=10;x+=0.65) if(Math.random()<0.93) seats.push([x+rand(-0.08,0.08),y,z]); }
  for(let r=0;r<4;r++){ const y=0.55+r*0.6; for(let z=-15;z<=0.5;z+=0.65) if(Math.random()<0.9){ seats.push([9.3+r*0.9,y,z]); seats.push([-9.3-r*0.9,y,z]); } }
  crowd.count=seats.length;
  const bodyGeo=new THREE.BoxGeometry(0.38,0.7,0.38), headGeo=new THREE.SphereGeometry(0.15,10,8);
  crowd.body=new THREE.InstancedMesh(bodyGeo,new THREE.MeshStandardMaterial({roughness:0.9}),crowd.count);
  crowd.head=new THREE.InstancedMesh(headGeo,new THREE.MeshStandardMaterial({roughness:0.9}),crowd.count);
  const jackets=['#d33','#36c','#eee','#fc3','#3a3','#c6c','#222','#f80','#0a7a3c','#c8102e'];
  const skins=['#f1c7a3','#d9a57c','#8d5a3b','#6b3e26','#f8d9c0','#4a2b1a'];
  const dummy=new THREE.Object3D(), col=new THREE.Color();
  seats.forEach((s,i)=>{ crowd.base.push(new THREE.Vector3(...s)); crowd.phase.push(Math.random()*Math.PI*2);
    dummy.position.set(s[0],s[1],s[2]); dummy.updateMatrix(); crowd.body.setMatrixAt(i,dummy.matrix); crowd.body.setColorAt(i,col.set(pick(jackets)));
    dummy.position.y+=0.5; dummy.updateMatrix(); crowd.head.setMatrixAt(i,dummy.matrix); crowd.head.setColorAt(i,col.set(pick(skins))); });
  scene.add(crowd.body, crowd.head);
  const bleach=new THREE.MeshStandardMaterial({color:0x2a2f3a, roughness:1});
  for(let r=0;r<6;r++){ const top=0.55+r*0.6-0.35; const m=box(21,top,0.9,bleach); m.position.set(0,top/2,-15.8-r*0.9); scene.add(m); }
  for(let r=0;r<4;r++){ const top=0.55+r*0.6-0.35; const a=box(0.9,top,16.5,bleach); a.position.set(9.3+r*0.9,top/2,-7.25); scene.add(a); const b=a.clone(); b.position.x*=-1; scene.add(b); }
}
function updateCrowd(dt,t){
  const dummy=new THREE.Object3D(); const ex=game.excite;
  for(let i=0;i<crowd.count;i++){ const b=crowd.base[i]; const lift=ex>0.03?Math.max(0,Math.sin(t*9+crowd.phase[i]))*0.35*ex:0;
    dummy.position.set(b.x,b.y+lift,b.z); dummy.updateMatrix(); crowd.body.setMatrixAt(i,dummy.matrix);
    dummy.position.y+=0.5; dummy.updateMatrix(); crowd.head.setMatrixAt(i,dummy.matrix); }
  crowd.body.instanceMatrix.needsUpdate=true; crowd.head.instanceMatrix.needsUpdate=true;
}

// ---- 3D scoreboard ----
const sb={c:document.createElement('canvas')}; sb.c.width=512; sb.c.height=200; sb.ctx=sb.c.getContext('2d');
sb.tex=new THREE.CanvasTexture(sb.c); sb.tex.colorSpace=THREE.SRGBColorSpace;
const sbMesh=new THREE.Mesh(new THREE.PlaneGeometry(6.4,2.5), new THREE.MeshBasicMaterial({map:sb.tex})); sbMesh.position.set(0,7.6,-19.5); scene.add(sbMesh);
let sbKey='';
function drawScoreboard(){
  const x=sb.ctx; x.fillStyle='#0b0b0b'; x.fillRect(0,0,512,200); x.strokeStyle='#555'; x.lineWidth=8; x.strokeRect(4,4,504,192);
  x.textAlign='center'; x.font='bold 40px monospace'; x.fillStyle='#5ce07f'; x.fillText('BIRD',128,60); x.fillStyle='#ff6b6b'; x.fillText('DR. J',384,60);
  x.fillStyle='#ff3b1f'; x.font='bold 96px monospace'; x.fillText(game.score[0],128,155); x.fillText(game.score[1],384,155);
  x.fillStyle='#aaa'; x.font='18px monospace'; x.fillText('SHOT',256,80); x.fillStyle='#ffcc33'; x.font='bold 40px monospace'; x.fillText(Math.ceil(Math.max(0,game.shotClock)),256,125);
  sb.tex.needsUpdate=true;
}

/* =========================================================
   PLAYERS
   ========================================================= */
const DEFS = [
  { name:'Larry Bird', short:'BIRD', team:'CELTICS', jersey:'#0a7a3c', trim:'#ffffff', number:'33', skin:'#f1c7a3', hair:'#e5c56a', hairStyle:'flat', shoot:0.68, dunk:0.2, speed:4.7, steal:0.3, shatter:0.2, color:'#5ce07f' },
  { name:'Dr. J',      short:'DR. J', team:'SIXERS', jersey:'#c8102e', trim:'#ffffff', number:'6',  skin:'#6b3e26', hair:'#1a1a1a', hairStyle:'afro', shoot:0.58, dunk:0.92, speed:5.5, steal:0.45, shatter:0.5, color:'#ff6b6b' },
];
const JANITOR_DEF = { name:'Janitor', short:'JANITOR', team:'MAINT.', jersey:'#4a5560', trim:'#cfd6dd', number:'', skin:'#e2b48c', hair:'#666', hairStyle:'cap', shoot:0, dunk:0, speed:2.4, steal:0, shatter:0, color:'#ccc' };

function makeJerseyTexture(color,number,text,textColor){
  const c=document.createElement('canvas'); c.width=c.height=256; const x=c.getContext('2d');
  x.fillStyle=color; x.fillRect(0,0,256,256); x.fillStyle=textColor; x.textAlign='center';
  x.font='bold 38px sans-serif'; x.fillText(text,128,70); x.font='bold 130px Impact, sans-serif'; x.fillText(number,128,205);
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
function makeLabel(text,color){
  const c=document.createElement('canvas'); c.width=256; c.height=64; const x=c.getContext('2d');
  x.font='bold 40px monospace'; x.textAlign='center'; x.lineWidth=6; x.strokeStyle='#000'; x.strokeText(text,128,46); x.fillStyle=color; x.fillText(text,128,46);
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;
  const s=new THREE.Sprite(new THREE.SpriteMaterial({map:t, depthTest:false})); s.scale.set(1.4,0.35,1); return s;
}

class Player {
  constructor(def, idx){
    this.def=def; this.idx=idx; this.g=new THREE.Group();
    const skin=new THREE.MeshStandardMaterial({color:def.skin, roughness:0.8});
    const jers=new THREE.MeshStandardMaterial({color:def.jersey, roughness:0.9});
    const trim=new THREE.MeshStandardMaterial({color:def.trim, roughness:0.9});
    const hairMat=new THREE.MeshStandardMaterial({color:def.hair, roughness:1});
    const jersTex=new THREE.MeshStandardMaterial({map:makeJerseyTexture(def.jersey,def.number,def.team,def.trim), roughness:0.9});
    const mkLeg=(x)=>{ const grp=new THREE.Group(); grp.position.set(x,0.95,0); const l=box(0.17,0.85,0.17,def.hairStyle==='cap'?jers:skin); l.position.y=-0.45; grp.add(l);
      const shoe=box(0.19,0.1,0.3,trim); shoe.position.set(0,-0.9,0.05); grp.add(shoe); this.g.add(grp); return grp; };
    this.legL=mkLeg(-0.13); this.legR=mkLeg(0.13);
    const shorts=box(0.52,0.32,0.3,jers); shorts.position.y=0.98; this.g.add(shorts);
    const torso=new THREE.Mesh(new THREE.BoxGeometry(0.5,0.62,0.28),[jers,jers,jers,jers,jersTex,jersTex]); torso.position.y=1.44; torso.castShadow=true; this.g.add(torso);
    const mkArm=(x)=>{ const grp=new THREE.Group(); grp.position.set(x,1.7,0); const a=box(0.12,0.65,0.12,skin); a.position.y=-0.33; grp.add(a);
      const hand=new THREE.Mesh(new THREE.SphereGeometry(0.075,10,8),skin); hand.position.y=-0.68; hand.castShadow=true; grp.add(hand); this.g.add(grp); return grp; };
    this.armL=mkArm(-0.33); this.armR=mkArm(0.33);
    const head=new THREE.Mesh(new THREE.SphereGeometry(0.15,16,12),skin); head.position.set(0,1.95,0.02); head.castShadow=true; this.g.add(head);
    if(def.hairStyle==='afro'){ const h=new THREE.Mesh(new THREE.SphereGeometry(0.21,16,12),hairMat); h.position.set(0,2.03,-0.07); h.castShadow=true; this.g.add(h);
      const m=box(0.13,0.03,0.03,hairMat); m.position.set(0,1.9,0.16); this.g.add(m); }
    else if(def.hairStyle==='flat'){ const h=box(0.32,0.12,0.32,hairMat); h.position.set(0,2.05,0); this.g.add(h); }
    else { const cap=box(0.33,0.1,0.34,new THREE.MeshStandardMaterial({color:0x2a2a2a})); cap.position.set(0,2.05,0); this.g.add(cap);
      const vis=box(0.3,0.03,0.16,new THREE.MeshStandardMaterial({color:0x2a2a2a})); vis.position.set(0,2.01,0.24); this.g.add(vis);
      const m=box(0.16,0.04,0.03,hairMat); m.position.set(0,1.89,0.16); this.g.add(m); }
    this.label=makeLabel(def.short,def.color); this.label.position.y=2.55; this.g.add(this.label);
    scene.add(this.g);
    this.pos=this.g.position; this.vy=0; this.facing=0; this.phase=0; this.moving=false;
    this.action='idle'; this.actionT=0; this.stealCd=0; this.aiSide=1; this.aiSideT=0; this.holdT=0;
  }
  get grounded(){ return this.pos.y<=0 && this.vy<=0; }
  jump(v){ if(this.grounded) this.vy=v; }
  local(x,y,z){ return this.g.localToWorld(new THREE.Vector3(x,y,z)); }
  distXZ(v){ return Math.hypot(this.pos.x-v.x, this.pos.z-v.z); }
  update(dt){
    if(this.pos.y>0 || this.vy>0){ this.vy+=G*dt; this.pos.y+=this.vy*dt;
      if(this.pos.y<=0){ this.pos.y=0; this.vy=0; if(['shoot','block','dunk'].includes(this.action)) this.action='idle'; } }
    this.g.rotation.y=this.facing;
    if(this.moving && this.pos.y===0) this.phase+=dt*11; else this.phase*=0.8;
    const sw=Math.sin(this.phase)*(this.moving?0.7:0.3);
    let lL=sw, lR=-sw, aL=-sw*0.8, aR=sw*0.8;
    if(this.pos.y>0){ lL=0.5; lR=-0.25; }
    switch(this.action){
      case 'shoot': aL=aR=-2.8; break;
      case 'dunk': aL=aR=-3.05; break;
      case 'block': aL=aR=-3.1; break;
      case 'dribble': aR=-0.9; aL=-0.25; break;
      case 'steal': aR=-1.6; break;
      case 'carry': aL=aR=-1.15; break;
      case 'sweep': aL=aR=-1.0+Math.sin(performance.now()*0.007)*0.15; break;
      case 'shake': aR=-2.5+Math.sin(performance.now()*0.03)*0.3; aL=-0.4; break;
    }
    const k=Math.min(1,dt*15);
    this.legL.rotation.x+=(lL-this.legL.rotation.x)*k; this.legR.rotation.x+=(lR-this.legR.rotation.x)*k;
    this.armL.rotation.x+=(aL-this.armL.rotation.x)*k; this.armR.rotation.x+=(aR-this.armR.rotation.x)*k;
  }
}
const players=[new Player(DEFS[0],0), new Player(DEFS[1],1)];
players[0].pos.set(-2,0,-6); players[1].pos.set(2,0,-6);

// janitor + broom
const janitor=new Player(JANITOR_DEF,2); janitor.g.visible=false;
const broom=new THREE.Group(); broom.position.set(0.32,1.05,0.45); broom.rotation.x=0.55;
{ const handle=new THREE.Mesh(new THREE.CylinderGeometry(0.02,0.02,1.6,8), new THREE.MeshStandardMaterial({color:0xb08850})); handle.position.y=-0.2; handle.castShadow=true; broom.add(handle);
  const head=box(0.55,0.09,0.12,new THREE.MeshStandardMaterial({color:0xd9c27a})); head.position.y=-1.0; broom.add(head);
  const bristles=box(0.55,0.12,0.1,new THREE.MeshStandardMaterial({color:0xe8d89a})); bristles.position.y=-1.1; broom.add(bristles); }
janitor.g.add(broom);

/* =========================================================
   BALL
   ========================================================= */
function makeBallTexture(){
  const c=document.createElement('canvas'); c.width=256; c.height=128; const x=c.getContext('2d');
  x.fillStyle='#e8631a'; x.fillRect(0,0,256,128); x.strokeStyle='#1a0d05'; x.lineWidth=5;
  x.beginPath(); x.moveTo(0,64); x.lineTo(256,64); x.stroke();
  [0,128].forEach(px=>{ x.beginPath(); x.moveTo(px,0); x.lineTo(px,128); x.stroke(); });
  [64,192].forEach(px=>{ x.beginPath(); for(let y=0;y<=128;y+=4){ const off=Math.sin(y/128*Math.PI)*28; y===0?x.moveTo(px+off,y):x.lineTo(px+off,y); } x.stroke(); });
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
const ball={ mesh:new THREE.Mesh(new THREE.SphereGeometry(0.12,24,16), new THREE.MeshStandardMaterial({map:makeBallTexture(), roughness:0.7})),
  pos:V3(0,0.12,-6), vel:V3(), state:'held', holder:players[0], t:0, T:0, make:false, start:V3(), v0:V3(), dribble:0, looseT:0 };
ball.mesh.castShadow=true; scene.add(ball.mesh);

/* =========================================================
   GLASS SHARDS
   ========================================================= */
const shardGeos=[];
for(let i=0;i<6;i++){ const sh=new THREE.Shape(); const n=3+Math.floor(Math.random()*2);
  for(let k=0;k<n;k++){ const a=k/n*Math.PI*2+rand(-0.3,0.3), r=rand(0.06,0.17); k?sh.lineTo(Math.cos(a)*r,Math.sin(a)*r):sh.moveTo(Math.cos(a)*r,Math.sin(a)*r); }
  shardGeos.push(new THREE.ShapeGeometry(sh)); }
const shardMat=new THREE.MeshStandardMaterial({color:0xdff4ff, transparent:true, opacity:0.75, roughness:0.05, metalness:0.2, side:THREE.DoubleSide});
let shards=[];
function shatterBoard(){
  board.visible=false; boardIntact=false; game.state='shatter'; game.timer=2.4; game.excite=1.6;
  sfx.glass(); setTimeout(()=>sfx.cheer(),300);
  setMsg('💥 SHATTERED BACKBOARD!!! 💥', 3);
  for(let i=0;i<120;i++){
    const s=new THREE.Mesh(pick(shardGeos),shardMat); s.castShadow=true;
    s.position.set(RIM.x+rand(-0.9,0.9), 3.425+rand(-0.52,0.52), BOARD_Z);
    s.rotation.set(rand(0,6),rand(0,6),rand(0,6));
    s.userData={ v:V3(s.position.x*1.6+rand(-1.5,1.5), rand(-1,3.5), rand(0.5,4.5)), w:V3(rand(-9,9),rand(-9,9),rand(-9,9)), onFloor:false, swept:false };
    scene.add(s); shards.push(s);
  }
}
function updateShards(dt){
  for(let i=shards.length-1;i>=0;i--){ const s=shards[i], u=s.userData;
    if(u.swept){ s.scale.multiplyScalar(Math.max(0,1-dt*7)); if(s.scale.x<0.05){ scene.remove(s); shards.splice(i,1); } continue; }
    if(u.onFloor) continue;
    u.v.y+=G*dt; s.position.addScaledVector(u.v,dt); s.rotation.x+=u.w.x*dt; s.rotation.y+=u.w.y*dt; s.rotation.z+=u.w.z*dt;
    if(s.position.y<0.01){ s.position.y=0.01; u.onFloor=true; s.rotation.set(-Math.PI/2,0,rand(0,6)); if(Math.random()<0.35) sfx.tink(); }
  }
}
function clearShards(){ shards.forEach(s=>scene.remove(s)); shards=[]; }

/* =========================================================
   GAME STATE
   ========================================================= */
const game={ state:'menu', score:[0,0], shotClock:24, offense:0, human:0, nextOffense:0, timer:0, excite:0, msgT:0 };
let shot=null, dunk=null, jan=null;
const msgEl=document.getElementById('msg');
function setMsg(t,dur=2){ msgEl.textContent=t; msgEl.style.opacity=1; game.msgT=dur; }

// ---- input ----
const keys=new Set(), justPressed=new Set();
addEventListener('keydown',e=>{ if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
  if(!keys.has(e.code)) justPressed.add(e.code); keys.add(e.code);
  if(e.code==='KeyM'){ A.muted=!A.muted; setMsg(A.muted?'MUTED':'SOUND ON',1); }
  if(e.code==='KeyR' && game.state!=='menu') startGame(game.human);
  if(game.state==='menu' && (e.code==='Digit1'||e.code==='Digit2')) startGame(e.code==='Digit1'?0:1); });
addEventListener('keyup',e=>keys.delete(e.code));
document.querySelectorAll('.card').forEach(c=>c.addEventListener('click',()=>startGame(+c.dataset.idx)));
document.getElementById('againBtn').addEventListener('click',()=>startGame(game.human));
addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });

function startGame(humanIdx){
  initAudio(); if(A.ctx.state==='suspended') A.ctx.resume();
  game.human=humanIdx; game.score=[0,0]; game.excite=0;
  document.getElementById('menu').classList.add('hidden'); document.getElementById('over').classList.add('hidden');
  clearShards(); board.visible=true; glassMat.opacity=0.4; boardIntact=true; janitor.g.visible=false; jan=null;
  players[0].label.material.map = makeLabel(humanIdx===0?'BIRD (YOU)':'BIRD','#5ce07f').material.map;
  players[1].label.material.map = makeLabel(humanIdx===1?'DR. J (YOU)':'DR. J','#ff6b6b').material.map;
  players.forEach(p=>{ p.label.scale.set(1.8,0.45,1); });
  resetPositions(Math.random()<0.5?0:1);
  setMsg(`You are ${DEFS[humanIdx].name}. Game to ${WIN}!`,3);
}
function resetPositions(off){
  game.offense=off; const o=players[off], d=players[1-off];
  o.pos.set(0,0,-6.2); d.pos.set(0,0,-8.6); o.facing=Math.PI; d.facing=0; o.vy=d.vy=0;
  o.action='dribble'; d.action='idle'; ball.state='held'; ball.holder=o; ball.looseT=0;
  game.shotClock=24; game.state='play'; shot=null; dunk=null;
  setMsg(`${o.def.short} ball`,1.2);
}
function gameOver(p){
  game.state='gameover'; sfx.buzzer(); sfx.cheer(); game.excite=2;
  document.getElementById('overTitle').textContent=`${p.def.name.toUpperCase()} WINS!`;
  document.getElementById('overSub').textContent=`${game.score[0]} – ${game.score[1]}  •  ${p.idx===game.human?'Nice work.':'Better luck next time.'}`;
  document.getElementById('over').classList.remove('hidden');
}

// ---- movement ----
function movePlayer(p,dx,dz,dt){
  const len=Math.hypot(dx,dz);
  if(len<0.01){ p.moving=false; return; }
  dx/=len; dz/=len; const sp=p.def.speed*(p.pos.y>0?0.5:1);
  p.pos.x=clamp(p.pos.x+dx*sp*dt,-COURT.halfW+0.3,COURT.halfW-0.3);
  p.pos.z=clamp(p.pos.z+dz*sp*dt,COURT.baseline+0.5,COURT.top);
  const nf=Math.atan2(dx,dz); let diff=Math.abs(nf-p.facing); if(diff>Math.PI) diff=Math.PI*2-diff;
  if(diff>2.0 && p.pos.y===0 && Math.random()<0.6) sfx.squeak();
  p.facing=nf; p.moving=true;
}
function separate(a,b){
  const dx=b.pos.x-a.pos.x, dz=b.pos.z-a.pos.z, d=Math.hypot(dx,dz);
  if(d<0.65 && d>1e-4){ const push=(0.65-d)/2, nx=dx/d, nz=dz/d; a.pos.x-=nx*push; a.pos.z-=nz*push; b.pos.x+=nx*push; b.pos.z+=nz*push; }
}
function refreshActions(){
  for(const p of players){ if(['shoot','dunk','carry','block','steal'].includes(p.action)) continue;
    p.action=(ball.state==='held'&&ball.holder===p)?'dribble':'idle'; }
}

// ---- shooting ----
function startShot(p){
  if(!p.grounded) return;
  p.action='shoot'; p.jump(4.6); p.facing=Math.atan2(RIM.x-p.pos.x,RIM.z-p.pos.z);
  shot={ shooter:p, released:false, aiReacted:false, points:p.distXZ(RIM)>6.75?3:2 };
}
function releaseShot(){
  const p=shot.shooter, d=players[1-p.idx], dist=p.distXZ(RIM), start=ball.pos.clone();
  let prob=p.def.shoot*(dist<2?1.35:1.35-(dist-2)*0.105);
  const dd=d.distXZ(p.pos);
  if(dd<1.7){ prob-=0.15; if(d.pos.y>0.05){ prob-=0.2;
    if(Math.random()<0.4){ // BLOCKED
      ball.state='loose'; ball.holder=null; ball.pos.copy(start); ball.vel.set(rand(-3,3),1.5,rand(2,5)); ball.looseT=0;
      sfx.thud(); setMsg(`${d.def.short} BLOCKS IT!`); game.excite=Math.max(game.excite,0.7); shot=null; return; } } }
  prob=clamp(prob,0.05,0.95);
  const make=Math.random()<prob;
  let target;
  if(make){ target=RIM.clone(); target.y+=0.15; }
  else { const a=rand(0,Math.PI*2); target=V3(RIM.x+Math.cos(a)*0.27,RIM.y+0.02,RIM.z+Math.sin(a)*0.27); }
  const T=clamp(0.8+dist*0.08,0.8,1.5);
  const v0=target.clone().sub(start); v0.y-=0.5*G*T*T; v0.divideScalar(T);
  ball.state='flight'; ball.start.copy(start); ball.v0.copy(v0); ball.t=0; ball.T=T; ball.make=make; ball.holder=null; ball.target=target;
  ball.points=shot.points; ball.shooter=p; shot=null;
}
function arriveShot(){
  const p=ball.shooter;
  if(ball.make){ ball.state='through'; ball.pos.set(RIM.x,RIM.y-0.02,RIM.z); sfx.swish();
    score(p,ball.points,ball.points===3?pick([`${p.def.short} FROM DOWNTOWN! +3`,`BANG! ${p.def.short} for THREE!`]):pick([`SWISH! ${p.def.short} +2`,`NOTHING BUT NET! ${p.def.short} +2`,`${p.def.short} hits the jumper! +2`])); }
  else { sfx.rim(); ball.state='loose'; ball.pos.copy(ball.target); ball.vel.set(rand(-2.5,2.5),rand(1.5,3.5),rand(1,4)); ball.looseT=0;
    setMsg(pick(['CLANK! Off the rim','No good!','Rattles out!']),1.2); if(Math.random()<0.3) sfx.boo(); }
}
function score(p,pts,label){
  game.score[p.idx]+=pts; game.excite=Math.max(game.excite,1); sfx.cheer(); setMsg(label,2.5);
  if(game.score[p.idx]>=WIN) gameOver(p); else { game.state='reset'; game.timer=2.2; game.nextOffense=1-p.idx; }
}

// ---- dunking ----
function startDunk(p){ if(!p.grounded) return; game.state='dunk'; dunk={p,phase:'approach'}; p.action='carry'; }
function updateDunk(dt){
  const p=dunk.p;
  if(dunk.phase==='approach'){
    p.action='carry'; const side=p.pos.x>=0?1:-1, tx=RIM.x+side*0.2, tz=RIM.z+0.95, dx=tx-p.pos.x, dz=tz-p.pos.z, dist=Math.hypot(dx,dz);
    if(dist>0.12){ const sp=p.def.speed*1.3; p.pos.x+=dx/dist*sp*dt; p.pos.z+=dz/dist*sp*dt; p.facing=Math.atan2(dx,dz); p.moving=true; }
    else { p.facing=Math.atan2(RIM.x-p.pos.x,RIM.z-p.pos.z); p.jump(6.3); p.action='dunk'; dunk.phase='air'; }
  } else if(dunk.phase==='air'){
    p.action='dunk'; p.pos.x+=(RIM.x-p.pos.x)*dt*2.5; p.pos.z+=(RIM.z+0.35-p.pos.z)*dt*2.5;
    if(p.pos.y>0.85 || p.vy<=0){
      ball.state='through'; ball.holder=null; ball.pos.set(RIM.x,RIM.y-0.05,RIM.z); sfx.slam(); game.excite=1.3;
      score(p,2,pick(['SLAM!','THUNDER DUNK!','POSTERIZED!','IN YOUR FACE!','TOMAHAWK JAM!','ROCK THE BABY!'])+` ${p.def.short} +2`);
      dunk=null;
      if(game.state==='reset' && Math.random()<p.def.shatter) shatterBoard();
    }
  }
}

// ---- steals / fouls ----
function attemptSteal(p){
  const h=ball.holder; if(!h || ball.state!=='held' || p.stealCd>0 || h===p) return;
  p.stealCd=1.1; p.action='steal'; p.actionT=0.35;
  if(p.distXZ(h.pos)<1.3){
    const r=Math.random();
    if(r<p.def.steal*0.5){ ball.holder=p; game.offense=p.idx; game.shotClock=24; h.action='idle'; setMsg(`STEAL by ${p.def.short}!`); game.excite=Math.max(game.excite,0.6); sfx.swish(); }
    else if(r>0.78){ sfx.whistle(); setMsg(`REACH-IN FOUL on ${p.def.short}`); game.state='reset'; game.timer=1.6; game.nextOffense=h.idx; if(Math.random()<0.5) sfx.boo(); }
  }
}

// ---- janitor sequence ----
function startJanitor(){
  game.state='janitor'; jan={phase:'enter',t:0,dir:-1,grumbleT:0}; janitor.g.visible=true;
  janitor.pos.set(11,0,-12.6); janitor.facing=-Math.PI/2;
  setMsg('Uh oh... here comes the janitor.',3); sfx.grumble(); if(Math.random()<0.6) sfx.boo();
}
function janWalk(tx,tz,dt){
  const dx=tx-janitor.pos.x, dz=tz-janitor.pos.z, d=Math.hypot(dx,dz);
  if(d<0.1){ janitor.moving=false; return true; }
  janitor.pos.x+=dx/d*janitor.def.speed*dt; janitor.pos.z+=dz/d*janitor.def.speed*dt; janitor.facing=Math.atan2(dx,dz); janitor.moving=true; return false;
}
function updateJanitor(dt){
  jan.t+=dt; jan.grumbleT-=dt; const J=janitor;
  if(jan.grumbleT<=0 && jan.phase!=='fix'){ sfx.grumble(); jan.grumbleT=rand(1.4,2.6); }
  switch(jan.phase){
    case 'enter': J.action='carry'; if(janWalk(1.8,-11.9,dt)){ jan.phase='sweep'; jan.t=0; jan.sweepT=0; } break;
    case 'sweep': {
      J.action='sweep'; J.moving=true; J.pos.x+=jan.dir*0.75*dt; J.facing=jan.dir>0?Math.PI/2:-Math.PI/2;
      if(J.pos.x<-1.8) jan.dir=1; if(J.pos.x>1.8) jan.dir=-1;
      broom.rotation.z=Math.sin(jan.t*7)*0.6;
      jan.sweepT-=dt; if(jan.sweepT<=0){ sfx.sweep(); jan.sweepT=0.45; }
      const tip=broom.localToWorld(V3(0,-1.05,0));
      for(const s of shards) if(!s.userData.swept && s.userData.onFloor && Math.hypot(s.position.x-tip.x,s.position.z-tip.z)<0.75) s.userData.swept=true;
      const left=shards.filter(s=>!s.userData.swept).length;
      if(jan.t>3 && (left===0 || jan.t>9)){ shards.forEach(s=>s.userData.swept=true); jan.phase='shake'; jan.t=0; broom.rotation.z=0;
        setMsg(pick(['Janitor: "HEY! Who\'s gonna pay for that?!"','Janitor: "Not AGAIN!"','Janitor: "I just cleaned that!"','Janitor: "You kids and your slam dunks..."']),3); }
      break; }
    case 'shake': {
      J.action='shake'; J.moving=false; const culprit=players[1-game.nextOffense]; J.facing=Math.atan2(culprit.pos.x-J.pos.x,culprit.pos.z-J.pos.z);
      if(jan.t>2.4){ jan.phase='fix'; jan.t=0; J.action='carry'; J.facing=Math.PI; board.visible=true; glassMat.opacity=0; sfx.hammer(); setMsg('Installing a new backboard...',2); }
      break; }
    case 'fix': glassMat.opacity=Math.min(0.4,jan.t*0.35); if(jan.t>1.4){ boardIntact=true; jan.phase='exit'; jan.t=0; } break;
    case 'exit': J.action='carry'; if(janWalk(11,-12.6,dt)){ J.g.visible=false; clearShards(); game.state='reset'; game.timer=0.4; } break;
  }
}

// ---- input & AI ----
function handleInput(dt){
  const h=players[game.human];
  if(!['shoot','dunk','carry'].includes(h.action)){
    let dx=0,dz=0;
    if(keys.has('KeyA')||keys.has('ArrowLeft')) dx-=1; if(keys.has('KeyD')||keys.has('ArrowRight')) dx+=1;
    if(keys.has('KeyW')||keys.has('ArrowUp')) dz-=1; if(keys.has('KeyS')||keys.has('ArrowDown')) dz+=1;
    movePlayer(h,dx,dz,dt);
    if(!h.moving && h.pos.y===0){ if(ball.state==='held'&&ball.holder===h) h.facing=Math.atan2(RIM.x-h.pos.x,RIM.z-h.pos.z);
      else if(ball.holder) h.facing=Math.atan2(ball.holder.pos.x-h.pos.x,ball.holder.pos.z-h.pos.z); }
  }
  if(justPressed.has('Space')){
    if(ball.state==='held'&&ball.holder===h){ if(h.distXZ(RIM)<2.6) startDunk(h); else startShot(h); }
    else if(h.grounded){ h.jump(4.8); h.action='block'; }
  }
  if(justPressed.has('ShiftLeft')||justPressed.has('ShiftRight')||justPressed.has('KeyX')) attemptSteal(h);
}
function updateAI(dt){
  const ai=players[1-game.human], hu=players[game.human];
  if(['shoot','dunk','carry'].includes(ai.action)) return;
  if(ball.state==='held' && ball.holder===ai){ // ---- offense
    const toRim=V3(RIM.x-ai.pos.x,0,RIM.z-ai.pos.z), dRim=toRim.length(); toRim.normalize();
    const dDef=ai.distXZ(hu.pos), toDef=V3(hu.pos.x-ai.pos.x,0,hu.pos.z-ai.pos.z);
    ai.aiSideT-=dt; if(ai.aiSideT<=0){ ai.aiSide=Math.random()<0.5?-1:1; ai.aiSideT=rand(0.7,1.6); }
    let mx=toRim.x, mz=toRim.z;
    if(dDef<1.9 && toDef.dot(toRim)>0){ mx+=-toRim.z*ai.aiSide*1.6; mz+=toRim.x*ai.aiSide*1.6; }
    if(ai.holdT>0){ ai.holdT-=dt; mx=mz=0; } else if(dRim>4 && Math.random()<0.35*dt) ai.holdT=rand(0.3,0.9);
    if(ai.grounded){ movePlayer(ai,mx,mz,dt); if(!ai.moving) ai.facing=Math.atan2(RIM.x-ai.pos.x,RIM.z-ai.pos.z); }
    if(ai.grounded){
      if(dRim<2.4){ if(game.shotClock<3 || Math.random()<(dDef<1.2?0.5:1)*(0.6+ai.def.dunk)*2.2*dt){ if(Math.random()<ai.def.dunk) startDunk(ai); else startShot(ai); } }
      else if(dRim<7.3 && dDef>2.1 && Math.random()<(ai.def.shoot*1.4)*dt) startShot(ai);
      else if(game.shotClock<2.5) startShot(ai);
    }
  } else if(ball.state==='held' && ball.holder===hu){ // ---- defense
    const toRim=V3(RIM.x-hu.pos.x,0,RIM.z-hu.pos.z).normalize();
    const tx=hu.pos.x+toRim.x*1.1, tz=hu.pos.z+toRim.z*1.1, dx=tx-ai.pos.x, dz=tz-ai.pos.z, d=Math.hypot(dx,dz);
    if(ai.grounded){ if(d>0.15) movePlayer(ai,dx,dz,dt); else ai.moving=false; ai.facing=Math.atan2(hu.pos.x-ai.pos.x,hu.pos.z-ai.pos.z); }
    if(shot && shot.shooter===hu && !shot.aiReacted){ shot.aiReacted=true; if(ai.grounded && ai.distXZ(hu.pos)<2.2 && Math.random()<0.55){ ai.jump(4.9); ai.action='block'; } }
    if(ai.distXZ(hu.pos)<1.25 && ai.stealCd<=0 && Math.random()<0.45*dt) attemptSteal(ai);
  } else if(ball.state==='loose' && ai.grounded){ movePlayer(ai,ball.pos.x-ai.pos.x,ball.pos.z-ai.pos.z,dt); }
}

// ---- ball ----
function pickUp(p){
  const wasOff=game.offense; ball.state='held'; ball.holder=p; ball.vel.set(0,0,0); game.offense=p.idx; game.shotClock=24; ball.looseT=0;
  players.forEach(q=>{ if(q.action==='block') q.action='idle'; });
  if(wasOff!==p.idx) setMsg(`${p.def.short} grabs the rebound`,1.2);
}
function updateBall(dt){
  const b=ball;
  if(b.state==='held'){
    const h=b.holder;
    if(h.action==='shoot'||h.action==='dunk'||h.action==='block') b.pos.copy(h.local(0.1,2.35,0.3));
    else if(h.action==='carry') b.pos.copy(h.local(0,1.35,0.45));
    else { const prev=Math.sin(b.dribble); b.dribble+=dt*9.5; const s=Math.sin(b.dribble);
      if(Math.sign(prev)!==Math.sign(s)) sfx.bounce(0.35);
      b.pos.copy(h.local(0.42,0.12+Math.abs(s)*0.78,0.3)); }
  } else if(b.state==='flight'){
    b.t+=dt;
    if(b.t>=b.T){ arriveShot(); }
    else { b.pos.copy(b.start).addScaledVector(b.v0,b.t); b.pos.y+=0.5*G*b.t*b.t; }
  } else if(b.state==='through'){
    b.pos.y-=3.2*dt; if(b.pos.y<2.35){ b.state='loose'; b.vel.set(rand(-1,1),-1,rand(0.5,1.5)); b.looseT=0; }
  } else if(b.state==='loose'){
    b.looseT+=dt; b.vel.y+=G*dt; b.pos.addScaledVector(b.vel,dt);
    if(b.pos.y<0.12){ b.pos.y=0.12; if(Math.abs(b.vel.y)>0.8) sfx.bounce(Math.min(0.6,Math.abs(b.vel.y)*0.1)); b.vel.y*=-0.6; b.vel.x*=0.8; b.vel.z*=0.8; if(Math.abs(b.vel.y)<0.5) b.vel.y=0; }
    if(b.pos.y<=0.121){ b.vel.x*=Math.max(0,1-2.5*dt); b.vel.z*=Math.max(0,1-2.5*dt); }
    if(Math.abs(b.pos.x)>COURT.halfW+0.4){ b.pos.x=Math.sign(b.pos.x)*(COURT.halfW+0.4); b.vel.x*=-0.6; }
    if(b.pos.z<COURT.baseline-0.2){ b.pos.z=COURT.baseline-0.2; b.vel.z*=-0.6; }
    if(b.pos.z>COURT.top+0.6){ b.pos.z=COURT.top+0.6; b.vel.z*=-0.6; }
    if(boardIntact && b.vel.z<0 && b.pos.z<BOARD_Z+0.14 && b.pos.z>BOARD_Z-0.1 && Math.abs(b.pos.x)<0.95 && b.pos.y>2.85 && b.pos.y<4){ b.pos.z=BOARD_Z+0.14; b.vel.z*=-0.7; sfx.thud(); }
    if(game.state==='play'){
      for(const p of players) if(p.distXZ(b.pos)<0.9 && b.pos.y<1.7 && p.pos.y<0.3){ pickUp(p); break; }
      if(b.looseT>6){ let n=players[0].distXZ(b.pos)<players[1].distXZ(b.pos)?players[0]:players[1]; pickUp(n); }
    }
    b.mesh.rotation.x+=b.vel.z*dt*4; b.mesh.rotation.z-=b.vel.x*dt*4;
  }
  b.mesh.position.copy(b.pos);
}

// ---- HUD / camera / ambient ----
const hudBird=document.getElementById('hudBird'), hudJ=document.getElementById('hudJ'), hudClock=document.getElementById('hudClock');
function updateHUD(){
  hudBird.textContent=`BIRD ${game.score[0]}${game.human===0?' (YOU)':''}`; hudJ.textContent=`DR. J ${game.score[1]}${game.human===1?' (YOU)':''}`;
  hudClock.textContent=`SHOT ${Math.ceil(Math.max(0,game.shotClock))}`;
  hudBird.classList.toggle('has',ball.holder===players[0]&&ball.state==='held'); hudJ.classList.toggle('has',ball.holder===players[1]&&ball.state==='held');
  const key=`${game.score[0]}-${game.score[1]}-${Math.ceil(game.shotClock)}`; if(key!==sbKey){ sbKey=key; drawScoreboard(); }
}
const camTarget=V3();
function updateCamera(dt){
  const fx=ball.pos.x*0.3; camera.position.x+=(fx-camera.position.x)*Math.min(1,dt*2); camera.position.y=8.5; camera.position.z=4.5;
  camTarget.set(camera.position.x*0.6,1.2,-9); camera.lookAt(camTarget);
}
function updateAmbient(){ if(!A.ctx) return; A.crowd.gain.value=A.muted?0:0.035+game.excite*0.22; A.crowdFilter.frequency.value=450+game.excite*1800; }

/* =========================================================
   MAIN LOOP
   ========================================================= */
let last=performance.now();
function tick(dt){
  const t=performance.now()/1000;
  game.excite=Math.max(0,game.excite-dt*0.45);
  if(game.msgT>0){ game.msgT-=dt; if(game.msgT<=0) msgEl.style.opacity=0; }
  updateCrowd(dt,t); updateShards(dt); updateAmbient();
  if(game.state==='menu'){ camera.position.x=Math.sin(t*0.25)*3; camera.lookAt(0,1.5,-9); players.forEach(p=>p.update(dt)); ball.mesh.position.copy(ball.pos); justPressed.clear(); return; }
  players.forEach(p=>p.moving=false);
  if(game.state==='play'){
    handleInput(dt); updateAI(dt); refreshActions();
    if(shot && !shot.released && shot.shooter.pos.y>0 && shot.shooter.vy<=0.6) releaseShot();
    if(ball.state==='held'){ game.shotClock-=dt; if(game.shotClock<=0){ sfx.buzzer(); setMsg('SHOT CLOCK VIOLATION!'); game.state='reset'; game.timer=1.6; game.nextOffense=1-game.offense; } }
  }
  else if(game.state==='dunk') updateDunk(dt);
  else if(game.state==='reset'){ game.timer-=dt; if(game.timer<=0) resetPositions(game.nextOffense); }
  else if(game.state==='shatter'){ game.timer-=dt; if(game.timer<=0) startJanitor(); }
  else if(game.state==='janitor') updateJanitor(dt);
  for(const p of players){ p.update(dt); p.stealCd-=dt; if(p.actionT>0){ p.actionT-=dt; if(p.actionT<=0&&p.action==='steal') p.action='idle'; } }
  if(janitor.g.visible) janitor.update(dt);
  if(players[0].pos.y===0 && players[1].pos.y===0) separate(players[0],players[1]);
  updateBall(dt); updateCamera(dt); updateHUD();
  justPressed.clear();
}
function loop(now){ requestAnimationFrame(loop); const dt=Math.min(0.05,(now-last)/1000); last=now; tick(dt); renderer.render(scene,camera); }
drawScoreboard(); loop(performance.now());
</script>
</body>
</html>