← back to Neon Baguette Battle

index.html

1474 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<title>Baguette Battle — Neon Paris</title>
<style>
  :root{ --gold:#c9a961; --cream:#f5f2ea; --pink:#ff3d9a; --cyan:#22e0ff; }
  *{ box-sizing:border-box; }
  html,body{ margin:0; height:100%; background:#05050a; }
  body{
    font-family:'Didot','Bodoni MT','Playfair Display',Georgia,serif;
    color:var(--cream);
    display:flex; justify-content:center; align-items:center; min-height:100vh;
    overflow:hidden;
    -webkit-user-select:none; user-select:none;
  }
  #stage{
    position:relative;
    width:min(96vw,960px);
    aspect-ratio:960/540;
    border-radius:14px;
    overflow:hidden;
    box-shadow:0 0 0 1px rgba(255,255,255,.06), 0 24px 80px rgba(255,61,154,.18), 0 8px 40px rgba(34,224,255,.12);
    background:#0d0d14;
  }
  #game{ position:absolute; inset:0; width:100%; height:100%; display:block; image-rendering:auto; }

  /* overlay screens */
  .screen{
    position:absolute; inset:0; z-index:5;
    display:flex; flex-direction:column; justify-content:center; align-items:center;
    text-align:center; padding:24px;
    background:radial-gradient(120% 90% at 50% 10%, rgba(20,16,40,.55), rgba(5,5,12,.9));
    backdrop-filter:blur(2px);
  }
  .screen.hidden{ display:none; }
  h1{
    font-size:clamp(34px,7vw,64px); letter-spacing:.14em; margin:0 0 6px;
    color:var(--cream);
    text-shadow:0 0 18px rgba(255,61,154,.65), 0 0 40px rgba(34,224,255,.35);
  }
  .sub{ letter-spacing:.35em; text-transform:uppercase; font-size:clamp(10px,1.6vw,14px);
        color:var(--gold); margin-bottom:26px; }
  p{ font-size:clamp(13px,1.8vw,16px); line-height:1.5; max-width:640px; color:#d9d3c6; }
  .btns{ display:flex; gap:16px; flex-wrap:wrap; justify-content:center; margin-top:22px; }
  button{
    font-family:inherit; font-size:clamp(14px,2vw,18px); letter-spacing:.08em;
    padding:14px 30px; border:1px solid rgba(201,169,97,.5); border-radius:8px;
    background:linear-gradient(180deg,rgba(201,169,97,.18),rgba(201,169,97,.05));
    color:var(--cream); cursor:pointer; transition:.18s;
  }
  button:hover{ border-color:var(--pink); box-shadow:0 0 22px rgba(255,61,154,.4); transform:translateY(-2px); }
  button.primary{ background:linear-gradient(180deg,#c9a961,#a4854a); color:#1a140a; border-color:#e7c987; font-weight:600; }
  .controls{ display:flex; gap:34px; flex-wrap:wrap; justify-content:center; margin-top:12px; font-size:13px; }
  .ctl{ text-align:left; }
  .ctl h3{ margin:0 0 8px; letter-spacing:.16em; text-transform:uppercase; font-size:12px; }
  .ctl.p1 h3{ color:#ff6b6b; } .ctl.p2 h3{ color:#6bb8ff; }
  .ctl table{ border-collapse:collapse; }
  .ctl td{ padding:2px 10px 2px 0; color:#cfc9bd; }
  .key{ display:inline-block; min-width:20px; padding:1px 7px; margin-right:2px;
        border:1px solid rgba(255,255,255,.25); border-radius:4px; background:rgba(255,255,255,.06);
        font-family:ui-monospace,Menlo,monospace; font-size:11px; color:#fff; }
  .tiny{ font-size:11px; letter-spacing:.2em; color:#8a8478; margin-top:26px; text-transform:uppercase; }
  .flag{ display:inline-block; width:34px; height:22px; border-radius:2px; vertical-align:middle;
         background:linear-gradient(90deg,#0055A4 0 33%,#fff 33% 66%,#EF4135 66% 100%); margin:0 8px; }

  /* character select */
  .selHead{ display:flex; align-items:center; justify-content:center; gap:22px; margin:2px 0 12px; flex-wrap:wrap; }
  .pTag{ font-size:13px; letter-spacing:.08em; padding:6px 14px; border-radius:6px; border:1px solid; min-width:170px; }
  .pTag.p1{ color:#ff8a8a; border-color:rgba(255,107,107,.5); }
  .pTag.p2{ color:#8ac2ff; border-color:rgba(107,184,255,.5); }
  .roster{ display:grid; grid-template-columns:repeat(3,1fr); gap:14px; max-width:620px; margin:2px auto; }
  @media(max-width:620px){ .roster{ grid-template-columns:repeat(2,1fr); max-width:340px; } }
  .card{ position:relative; background:rgba(255,255,255,.04); border:2px solid rgba(255,255,255,.1);
         border-radius:10px; padding:8px 8px 10px; cursor:pointer; transition:.12s; }
  .card:hover{ border-color:rgba(201,169,97,.6); }
  .card canvas{ width:72px; height:90px; display:block; margin:0 auto 4px; }
  .card .cname{ font-size:13px; font-weight:600; letter-spacing:.02em; color:#f2efe6; text-align:center; line-height:1.15;
                min-height:30px; display:flex; align-items:center; justify-content:center; font-family:Georgia,serif; }
  .card .stat{ display:flex; align-items:center; gap:6px; font-size:11px; color:#c3bdb0; margin:3px 4px; letter-spacing:.06em; }
  .card .stat:first-of-type{ margin-top:7px; padding-top:7px; border-top:1px solid rgba(255,255,255,.09); }
  .card .stat b{ width:30px; display:inline-block; }
  .card .bar{ position:relative; flex:1; height:7px; background:rgba(255,255,255,.10); border-radius:3px; overflow:hidden; }
  .card .bar i{ display:block; height:100%; }
  .card .bar.spd i{ background:linear-gradient(90deg,#17a2bd,#22e0ff); }
  .card .bar.pwr i{ background:linear-gradient(90deg,#c02d78,#ff3d9a); }
  .card .bar.def i{ background:linear-gradient(90deg,#9a7d3a,#e7c987); }
  .card .bar::after{ content:''; position:absolute; inset:0; pointer-events:none;
                     background:repeating-linear-gradient(90deg, transparent 0 19.4%, rgba(0,0,0,.5) 19.4% 20%); }
  .card.p1cursor{ border:3px solid #ff6b6b; box-shadow:0 0 18px rgba(255,61,90,.6); transform:scale(1.05); }
  .card.p2cursor{ border:3px solid #6bb8ff; box-shadow:0 0 18px rgba(34,120,255,.6); transform:scale(1.05); }
  .card.p1cursor.p2cursor{ border-color:#c9a961; box-shadow:0 0 20px rgba(201,169,97,.65); }
  .card .rdy{ position:absolute; top:5px; font-size:11px; font-weight:700; padding:2px 8px; border-radius:5px; z-index:2; }
  .card .rdy.r1{ left:5px; background:#c0392b; color:#fff; }
  .card .rdy.r2{ right:5px; background:#2c6fb0; color:#fff; }

  /* touch controls (mobile) */
  #touch{ position:absolute; inset:0; z-index:6; display:none; pointer-events:none; }
  body.touch #touch.on{ display:block; }
  .tc{ position:absolute; bottom:12px; display:flex; flex-wrap:wrap; gap:8px; pointer-events:none; }
  .tc-left{ left:12px; width:118px; }
  .tc-right{ right:12px; width:190px; justify-content:flex-end; }
  .tbtn{ pointer-events:auto; -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;
         touch-action:none; width:52px; height:52px; border-radius:50%;
         border:1px solid rgba(255,255,255,.32); background:rgba(18,14,28,.5);
         color:#f5f2ea; font:600 11px/1 Georgia,serif; -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
         display:flex; align-items:center; justify-content:center; box-shadow:0 2px 10px rgba(0,0,0,.4); }
  .tbtn.wide{ width:55px; height:44px; border-radius:12px; font-size:11px; letter-spacing:.04em; }
  .tbtn.atk{ width:56px; height:56px; font-size:12px; }
  .tbtn[data-k="a"], .tbtn[data-k="d"]{ font-size:20px; }
  .tbtn:active{ background:rgba(201,169,97,.55); color:#1a140a; transform:scale(.93); }
  #rotate{ display:none; }
  @media (orientation:portrait) and (max-width:820px){
    body.touch #rotate{ position:fixed; inset:0; z-index:30; display:flex; align-items:center; justify-content:center;
      background:rgba(5,5,12,.92); color:#f5f2ea; font-size:20px; letter-spacing:.06em; text-align:center; padding:24px; }
  }
</style>
</head>
<body>
<div id="stage">
  <canvas id="game" width="960" height="540"></canvas>

  <!-- START SCREEN -->
  <div class="screen" id="menu">
    <h1>BAGUETTE BATTLE</h1>
    <div class="sub"><span class="flag"></span> Neon&nbsp;Paris&nbsp;Combat&nbsp;Arena <span class="flag"></span></div>
    <p>Two Parisians settle their differences with baguettes beneath a neon Eiffel Tower.
       Poke, whack, block — then <b>launch your baguette like a missile</b>. Best of 3 rounds wins.</p>
    <div class="btns">
      <button class="primary" id="btn1p">1 PLAYER · vs&nbsp;CPU</button>
      <button id="btn2p">2 PLAYERS · local</button>
    </div>
    <div class="controls">
      <div class="ctl p1"><h3>🥖 Player 1</h3>
        <table>
          <tr><td>Move</td><td><span class="key">A</span><span class="key">D</span></td></tr>
          <tr><td>Jump</td><td><span class="key">W</span></td></tr>
          <tr><td>Block</td><td><span class="key">S</span></td></tr>
          <tr><td>Poke (baguette)</td><td><span class="key">Space</span><span class="key">F</span></td></tr>
          <tr><td>Whack</td><td><span class="key">G</span></td></tr>
          <tr><td>Throw baguette 🥖</td><td><span class="key">Return</span><span class="key">H</span></td></tr>
          <tr><td>Rocket 🚀</td><td><span class="key">R</span></td></tr>
          <tr><td>Bomb 💣</td><td><span class="key">B</span></td></tr>
        </table>
      </div>
      <div class="ctl p2"><h3>🥖 Player 2</h3>
        <table>
          <tr><td>Move</td><td><span class="key">←</span><span class="key">→</span></td></tr>
          <tr><td>Jump</td><td><span class="key">↑</span></td></tr>
          <tr><td>Block</td><td><span class="key">↓</span></td></tr>
          <tr><td>Poke (baguette)</td><td><span class="key">,</span></td></tr>
          <tr><td>Whack</td><td><span class="key">.</span></td></tr>
          <tr><td>Throw baguette 🥖</td><td><span class="key">/</span></td></tr>
          <tr><td>Rocket 🚀</td><td><span class="key">;</span></td></tr>
          <tr><td>Bomb 💣</td><td><span class="key">'</span></td></tr>
        </table>
      </div>
    </div>
    <div class="tiny">1-Player also: <span class="key">←</span><span class="key">→</span> move · <span class="key">↑</span> jump · <span class="key">↓</span> block · <span class="key">Space</span> poke · <span class="key">Return</span> throw · <span class="key">R</span> rocket · <span class="key">B</span> bomb &nbsp;·&nbsp; <span class="key">P</span> pause · <span class="key">M</span> mute</div>
  </div>

  <!-- CHARACTER SELECT -->
  <div class="screen hidden" id="select">
    <h1 style="font-size:clamp(24px,4.6vw,40px);margin-bottom:2px;">CHOOSE YOUR BAGUETTE</h1>
    <div class="selHead">
      <div class="pTag p1" id="p1tag">P1: —</div>
      <div class="sub" style="margin:0;"><span class="flag"></span></div>
      <div class="pTag p2" id="p2tag">P2: —</div>
    </div>
    <div class="roster" id="roster"></div>
    <div class="btns" style="margin-top:14px;">
      <button class="primary" id="btnStart" disabled>SELECT FIGHTERS</button>
      <button id="btnSelBack">BACK</button>
    </div>
    <div class="tiny" id="selHint"></div>
  </div>

  <!-- RESULT SCREEN -->
  <div class="screen hidden" id="result">
    <h1 id="resultTitle">PLAYER 1 WINS</h1>
    <div class="sub" id="resultSub">Magnifique!</div>
    <div class="btns">
      <button class="primary" id="btnRematch">REMATCH</button>
      <button id="btnMenu">MAIN MENU</button>
    </div>
  </div>

  <!-- PAUSE SCREEN -->
  <div class="screen hidden" id="pause">
    <h1>PAUSED</h1>
    <div class="btns">
      <button class="primary" id="btnResume">RESUME</button>
      <button id="btnQuit">QUIT TO MENU</button>
    </div>
    <div class="tiny">Press&nbsp;<span class="key">P</span>&nbsp;to resume</div>
  </div>

  <!-- TOUCH CONTROLS (mobile) — drive Player 1 -->
  <div id="touch" aria-hidden="true">
    <div class="tc tc-left">
      <button class="tbtn wide" data-k="w">JUMP</button>
      <button class="tbtn wide" data-k="s">BLOCK</button>
      <button class="tbtn" data-k="a">◀</button>
      <button class="tbtn" data-k="d">▶</button>
    </div>
    <div class="tc tc-right">
      <button class="tbtn atk" data-k="f">POKE</button>
      <button class="tbtn atk" data-k="g">WHACK</button>
      <button class="tbtn atk" data-k="h">🥖</button>
      <button class="tbtn atk" data-k="r">🚀</button>
      <button class="tbtn atk" data-k="b">💣</button>
    </div>
  </div>
  <div id="rotate">↺ Rotate your device to landscape</div>
</div>

<script>
"use strict";
(() => {
  const cv = document.getElementById('game');
  const ctx = cv.getContext('2d');
  const W = cv.width, H = cv.height;
  const GROUND = H - 92;         // y of the floor line
  const GRAV = 0.9;
  const ARENA_L = 60, ARENA_R = W - 60;

  // ---------- Audio (tiny WebAudio synth, no assets) ----------
  const Audio = (() => {
    let ac = null, muted = false;
    const ensure = () => { if (!ac) { try { ac = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){} } return ac; };
    function tone(freq, dur, type='square', vol=0.15, slideTo=null){
      if (muted) return; const a = ensure(); if(!a) return;
      const o = a.createOscillator(), g = a.createGain();
      o.type = type; o.frequency.value = freq;
      if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, a.currentTime + dur);
      g.gain.value = vol;
      g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + dur);
      o.connect(g); g.connect(a.destination);
      o.start(); o.stop(a.currentTime + dur);
    }
    function noise(dur=0.12, vol=0.2){
      if (muted) return; const a = ensure(); if(!a) return;
      const n = a.createBufferSource();
      const buf = a.createBuffer(1, a.sampleRate*dur, a.sampleRate);
      const d = buf.getChannelData(0);
      for (let i=0;i<d.length;i++) d[i] = (Math.random()*2-1) * (1 - i/d.length);
      n.buffer = buf;
      const g = a.createGain(); g.gain.value = vol;
      g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + dur);
      const f = a.createBiquadFilter(); f.type='bandpass'; f.frequency.value=1400;
      n.connect(f); f.connect(g); g.connect(a.destination); n.start();
    }
    // ---- La Marseillaise (opening) played on the melody synth ----
    let anthemStarted=false, anthemOsc=[];
    const NF = { 'D4':293.66, 'F#4':369.99, 'G4':392.00, 'A4':440.00, 'B4':493.88, 'C5':523.25, 'D5':587.33 };
    const ANTHEM = [ // [note, beats] — "Allons enfants de la patrie, le jour de gloire est arrivé..."
      ['D4',.5],['D4',.75],['D4',.25],['G4',1],['G4',.5],['A4',.5],['A4',1],
      ['D5',.75],['B4',.25],['G4',.5],['G4',.5],['B4',.75],['G4',.25],['C5',1],['A4',1],
      ['F#4',.5],['G4',.5],['G4',.5],['A4',.5],['B4',.75],['B4',.25],['B4',.5],['C5',1],
      ['B4',.5],['B4',.5],['A4',.5],['A4',.5],['B4',.75],['C5',.25],['C5',.5],['C5',.5],['D5',1],['C5',.5],['B4',1.5]
    ];
    function _scheduleAnthem(a){
      const beat=0.42; let t=a.currentTime+0.12;
      const master=a.createGain(); master.gain.value=0.9; master.connect(a.destination);
      for (const [n,b] of ANTHEM){
        const f=NF[n], dur=b*beat;
        const o=a.createOscillator(), g=a.createGain(), lp=a.createBiquadFilter();
        o.type='sawtooth'; o.frequency.value=f;
        lp.type='lowpass'; lp.frequency.value=2200;
        g.gain.setValueAtTime(0.0001, t);
        g.gain.exponentialRampToValueAtTime(0.13, t+0.03);
        g.gain.setValueAtTime(0.13, t+dur*0.72);
        g.gain.exponentialRampToValueAtTime(0.0001, t+dur*0.99);
        o.connect(lp); lp.connect(g); g.connect(master);
        o.start(t); o.stop(t+dur+0.02);
        anthemOsc.push(o);
        t+=dur;
      }
    }
    function startAnthem(){
      if (anthemStarted || muted) return;
      const a=ensure(); if(!a) return;
      const go=()=>{ if(anthemStarted) return; anthemStarted=true; _scheduleAnthem(a); };
      if (a.state==='suspended') a.resume().then(go).catch(()=>{}); else go();
    }
    function stopAnthem(){ for(const o of anthemOsc){ try{o.stop();}catch(e){} } anthemOsc=[]; }
    return {
      poke:  () => tone(520, .08, 'square', .12, 380),
      swing: () => { tone(180,.18,'sawtooth',.16,90); noise(.14,.14); },
      block: () => tone(240, .1, 'triangle', .14),
      hurt:  () => { noise(.16,.22); tone(140,.15,'sawtooth',.12,70); },
      blast: () => { tone(660,.25,'sawtooth',.16,120); noise(.2,.16); },
      boom:  () => { tone(90,.4,'sawtooth',.2,40); noise(.35,.3); },
      jump:  () => tone(300,.12,'sine',.1,540),
      ko:    () => { tone(120,.5,'sawtooth',.2,50); noise(.4,.2); },
      bell:  () => { tone(880,.5,'sine',.12); tone(1320,.5,'sine',.06); },
      toggle: () => { muted = !muted; if (muted) stopAnthem(); return muted; },
      resume: () => { const a = ensure(); if (a && a.state === 'suspended') a.resume(); },
      startAnthem, stopAnthem,
      get muted(){ return muted; }
    };
  })();

  // ---------- Voice (American hamming a French accent, via a French TTS voice) ----------
  const Voice = (() => {
    let voice = null, enabled = true, lastAt = 0;
    const has = ('speechSynthesis' in window);
    function pick(){
      if (!has) return;
      const vs = speechSynthesis.getVoices();
      voice = vs.find(v => /fr-FR|fr_FR/i.test(v.lang))
           || vs.find(v => /^fr/i.test(v.lang))
           || vs.find(v => /(français|france|thomas|amelie|aur[eé]lie)/i.test(v.name))
           || vs[0] || null;
    }
    if (has){ pick(); speechSynthesis.onvoiceschanged = pick; }
    const rnd = a => a[Math.floor(Math.random()*a.length)];
    function say(text, {rate=0.95, pitch=1.1, force=false} = {}){
      if (!enabled || Audio.muted || !has) return;
      const now = performance.now();
      if (!force && now - lastAt < 750) return;   // throttle chatter
      lastAt = now;
      try {
        if (force) speechSynthesis.cancel();
        const u = new SpeechSynthesisUtterance(text);
        if (voice) u.voice = voice;
        u.lang = voice ? voice.lang : 'fr-FR';
        u.rate = rate; u.pitch = pitch; u.volume = 1;
        speechSynthesis.speak(u);
      } catch(e){}
    }
    return {
      // phonetic English so a French voice reads it with the accent
      taunt:  () => say(rnd(["Take zat!","Ooh la la!","On garde!","Zut alors!","Eat my baguette!","Ha! Too slow, mon ami!"]), {pitch:1.15}),
      hurt:   () => say(rnd(["Oh non!","Mon dieu!","Sacrebleu!","Ze pain!","Aïe!"]), {pitch:1.25, rate:1.05}),
      block:  () => say(rnd(["Non non non!","Not today!"]), {pitch:1.15}),
      blast:  () => say(rnd(["Baguette missile! Allez!","Fire ze baguette!","Incoming! Ha ha!"]), {rate:0.95, pitch:1.05, force:true}),
      baguette: () => say(rnd(["Throw ze baguette!","Catch, mon ami!","Voilà!"]), {pitch:1.1, force:true}),
      rocket:   () => say(rnd(["Rocket! Allez!","Boom!","Fire ze rocket!"]), {rate:0.95, force:true}),
      bomb:     () => say(rnd(["Bombe!","Boom boom!","Ha! Ze bomb!"]), {pitch:1.15, force:true}),
      round:  () => say("Allez!", {force:true, pitch:1.1}),
      fight:  () => say("Fight, mon ami!", {force:true, pitch:1.15}),
      ko:     () => say("Oh la la! Magnifique!", {force:true, pitch:1.05}),
      win:    () => say(rnd(["Champion de Paris!","Formidable!","Très bien! Ze winner!"]), {force:true}),
      hazard: () => say(rnd(["Incoming pie!","Look out! Ze french roll!","Duck, mon ami!"]), {pitch:1.15}),
      stop:   () => { if (has) try { speechSynthesis.cancel(); } catch(e){} },
      set enabled(v){ enabled = v; if (!v) this.stop(); },
      get enabled(){ return enabled; }
    };
  })();

  // ---------- Input ----------
  const keys = {};
  // maps are arrays so an action can have several keys.
  //   poke = baguette jab · swing = whack · baguette = throw baguette · rocket = launch rocket (needs meter) · bomb = lob bomb
  const P1_ATTACK = {
    poke:     ['f','j',' ','shift'],                 // Space = poke with baguette
    swing:    ['g','k'],                             // whack
    baguette: ['h','enter','backspace','delete'],    // Return = throw baguette
    rocket:   ['r','meta','control'],                // R = rocket
    bomb:     ['b','alt'],                           // B = bomb
  };
  const P1MAP_1P  = { left:['a','arrowleft'], right:['d','arrowright'], jump:['w','arrowup'], block:['s','arrowdown'], ...P1_ATTACK };
  const P1MAP_2P  = { left:['a'], right:['d'], jump:['w'], block:['s'], ...P1_ATTACK };  // arrows belong to P2 in 2P
  const P2MAP     = { left:['arrowleft'], right:['arrowright'], jump:['arrowup'], block:['arrowdown'],
                      poke:[','], swing:['.'], baguette:['/'], rocket:[';'], bomb:["'"] };
  let   p1map     = P1MAP_1P;
  addEventListener('keydown', e => {
    const k = e.key.toLowerCase();
    if (['arrowup','arrowdown','arrowleft','arrowright',' ','/','backspace','delete'].includes(k)) e.preventDefault();
    if (!keys[k]) keys['_pressed_'+k] = true;   // edge trigger
    keys[k] = true;
    if (state === 'select'){
      if (k==='a') selMove(1,-1);
      else if (k==='d') selMove(1,1);
      else if (k==='f') selReady(1);
      else if (mode==='2p' && k==='arrowleft')  selMove(2,-1);
      else if (mode==='2p' && k==='arrowright') selMove(2,1);
      else if (mode==='2p' && k===',') selReady(2);
      else if (k==='enter' || k===' ') beginSelectedMatch();
      else if (k==='escape'){ state='menu'; hideScreens(); document.getElementById('menu').classList.remove('hidden'); }
      return;
    }
    if (k === 'p') togglePause();
    if (k === 'm') { const m = Audio.toggle(); Voice.enabled = !m; }
  });
  addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
  const pressed = (k) => { if (keys['_pressed_'+k]) { keys['_pressed_'+k] = false; return true; } return false; };

  // ---------- Fighter ----------
  // Selectable roster. spd/pow/def are 1..5 (shown as bars); converted to gameplay factors in Fighter.
  const ROSTER = [
    { id:'rouge',    name:'Baguette Rouge', spd:3, pow:3, def:3,
      colors:{ crust:'#d9a441', crustDk:'#a9761f', accent:'#ff4d4d', beret:'#c0392b', glow:'#ff6b6b' } },
    { id:'bleu',     name:'Baguette Bleue', spd:3, pow:3, def:3,
      colors:{ crust:'#e0b45a', crustDk:'#b08228', accent:'#4d9dff', beret:'#2c6fb0', glow:'#6bb8ff' } },
    { id:'costaud',  name:'Le Costaud',     spd:2, pow:5, def:4,
      colors:{ crust:'#c98a3e', crustDk:'#8a5a17', accent:'#ff9a3d', beret:'#7a3b12', glow:'#ffb347' } },
    { id:'petit',    name:'Petit Éclair',   spd:5, pow:2, def:2,
      colors:{ crust:'#f0d488', crustDk:'#c9a94e', accent:'#ffe14d', beret:'#e0a800', glow:'#ffe066' } },
    { id:'sourdough',name:'Pain de Campagne',spd:2, pow:4, def:5,
      colors:{ crust:'#b8894c', crustDk:'#7c561f', accent:'#d8b48c', beret:'#5b3a1a', glow:'#e6c79a' } },
    { id:'noir',     name:'Baguette Noire',  spd:4, pow:5, def:1,
      colors:{ crust:'#8a6a3a', crustDk:'#4a3418', accent:'#ff3d9a', beret:'#141414', glow:'#ff3d9a' } },
  ];

  class Fighter {
    constructor(x, side, cfg, human){
      this.x = x; this.y = GROUND; this.vx = 0; this.vy = 0;
      this.w = 58; this.h = 118;
      this.side = side;                 // 'p1' | 'p2' (controls + facing)
      this.cfg = cfg;
      this.name = cfg.name;
      this.colors = cfg.colors;
      this.spd  = 3.4 + cfg.spd * 0.42; // movement px/frame
      this.powF = 0.82 + cfg.pow * 0.07;// outgoing-damage multiplier
      this.defF = 1.18 - cfg.def * 0.06;// incoming-damage/knockback multiplier (lower = tankier)
      this.human = human;               // false => CPU
      this.facing = side === 'p1' ? 1 : -1;
      this.reset();
      this.rounds = 0;
    }
    reset(){
      this.hp = 100; this.meter = 0;
      this.vx = 0; this.vy = 0; this.onGround = true; this.y = GROUND;
      this.state = 'idle';              // idle|walk|jump|poke|swing|block|hurt|ko
      this.stateTime = 0;
      this.attack = null;               // {type,startup,active,recover,t,hit,dmg,reach,kb}
      this.hurtTimer = 0; this.blockHold = 0; this.invuln = 0;
      this.animT = Math.random()*10;
      this.aiTimer = 0; this.aiIntent = 'idle';
      this.armSwing = 0;                // visual arm extension 0..1
      this.cd = { baguette:0, bomb:0 }; // per-weapon cooldowns (rocket uses the meter)
    }
    get top(){ return this.y - this.h; }
    get cx(){ return this.x; }
    get cy(){ return this.y - this.h*0.5; }
    get alive(){ return this.hp > 0; }

    hurtbox(){ return { x:this.x - this.w/2, y:this.y - this.h, w:this.w, h:this.h }; }

    // attack hitbox in front of the fighter, when in active frames
    hitbox(){
      if (!this.attack || this.attack.phase !== 'active') return null;
      const reach = this.attack.reach;
      const hy = this.attack.type === 'swing' ? this.y - this.h*0.62 : this.y - this.h*0.5;
      const hh = this.attack.type === 'swing' ? 64 : 34;
      const hx = this.facing === 1 ? this.x + this.w/2 : this.x - this.w/2 - reach;
      return { x:hx, y:hy - hh/2, w:reach, h:hh };
    }

    startAttack(type){
      if (this.attack || this.state==='hurt' || this.state==='ko' || !this.onGround) return;
      const THROW = { baguette:14, rocket:16, bomb:15 };   // recover frames per thrown weapon
      if (type in THROW){
        if (type === 'rocket'){ if (this.meter < 100) return; this.meter = 0; }  // rocket costs the meter
        else { if (this.cd[type] > 0) return; this.cd[type] = (type==='baguette' ? 48 : 90); } // cooldown
        spawnProjectile(this, type);
        if (type==='bomb'){ Audio.swing(); Voice.bomb(); }
        else if (type==='rocket'){ Audio.blast(); Voice.rocket(); }
        else { Audio.blast(); Voice.baguette(); }
        this.attack = { type, phase:'recover', t:0, recover:THROW[type] };
        this.state = 'poke'; this.stateTime = 0; this.armSwing = 1;
        return;
      }
      const spec = type === 'poke'
        ? { startup:4, active:5, recover:9,  dmg:6,  reach:52, kb:5,  self:2.5 }
        : { startup:9, active:6, recover:20, dmg:15, reach:74, kb:12, self:5 };
      this.attack = { type, phase:'startup', t:0, hit:false, ...spec };
      this.state = type; this.stateTime = 0;
      if (type==='poke') Audio.poke(); else Audio.swing();
      if (type==='swing' && Math.random()<0.5) Voice.taunt();
      else if (type==='poke' && Math.random()<0.22) Voice.taunt();
    }

    takeHit(dmg, kb, fromDir){
      if (this.invuln > 0 || this.state==='ko') return false;
      dmg *= this.defF; kb *= this.defF;   // defense stat: tankier fighters take less
      const blocking = this.state === 'block';
      let dealt = dmg;
      if (blocking){
        dealt = Math.ceil(dmg * 0.22);           // chip damage
        kb *= 0.4;
        Audio.block(); if (Math.random()<0.45) Voice.block();
        spawnSpark(this.x + fromDir* -this.w*0.4, this.cy, '#bfe9ff', 6);
        this.meter = Math.min(100, this.meter + dmg*0.5);
      } else {
        Audio.hurt(); if (Math.random()<0.55) Voice.hurt();
        this.state = 'hurt'; this.stateTime = 0; this.hurtTimer = 16;
        this.attack = null;
        spawnSpark(this.x + fromDir* -this.w*0.4, this.cy, '#ffd27a', 14);
        this.meter = Math.min(100, this.meter + dmg*0.8);
      }
      this.hp = Math.max(0, this.hp - dealt);
      this.vx += fromDir * kb;
      if (!blocking){ this.vy = -Math.min(9, kb*0.7); this.onGround = false; }
      if (this.hp <= 0){ this.state='ko'; this.stateTime=0; this.attack=null; }
      shake = Math.min(16, shake + (blocking?3:8));
      return true;
    }

    controlHuman(map){
      if (this.state==='hurt'||this.state==='ko') return;
      const down = a => a && a.some(k => keys[k]);
      const hit  = a => { if (!a) return false; let h=false; for (const k of a){ if (keys['_pressed_'+k]){ keys['_pressed_'+k]=false; h=true; } } return h; };
      const speed = this.spd;
      const L = down(map.left), R = down(map.right);
      if (!this.attack){
        if (L && !R){ this.vx = -speed; this.facing = -1; }
        else if (R && !L){ this.vx = speed; this.facing = 1; }
      }
      if (down(map.block) && this.onGround && !this.attack){ this.state = 'block'; }
      else if (this.state==='block'){ this.state = 'idle'; }
      if (hit(map.jump) && this.onGround && !this.attack){ this.vy = -16; this.onGround = false; Audio.jump(); }
      if (hit(map.poke))     this.startAttack('poke');
      if (hit(map.swing))    this.startAttack('swing');
      if (hit(map.baguette)) this.startAttack('baguette');
      if (hit(map.rocket))   this.startAttack('rocket');
      if (hit(map.bomb))     this.startAttack('bomb');
    }

    controlAI(foe){
      if (this.state==='hurt'||this.state==='ko') return;
      this.aiTimer--;
      const dist = foe.x - this.x;
      const adist = Math.abs(dist);
      this.facing = dist >= 0 ? 1 : -1;
      const speed = this.spd;

      // re-decide intent periodically
      if (this.aiTimer <= 0){
        this.aiTimer = 14 + Math.floor(Math.random()*22);
        const r = Math.random();
        if (this.hp < 28 && r < 0.35) this.aiIntent = 'retreat';
        else if (adist > 150) this.aiIntent = 'approach';
        else if (adist < 95){
          this.aiIntent = r < 0.5 ? 'attack' : (r < 0.72 ? 'block' : (r<0.86?'retreat':'jump'));
        } else this.aiIntent = r < 0.62 ? 'approach' : 'attack';
        if (this.meter >= 100 && adist < 260 && r < 0.6) this.aiIntent = 'blast';
      }

      // react to incoming attack -> chance to block
      if (foe.attack && foe.attack.phase!=='recover' && adist < 110 && Math.random() < 0.14){
        this.aiIntent = 'block';
      }

      if (this.state === 'block' && this.aiIntent !== 'block') this.state = 'idle';

      switch(this.aiIntent){
        case 'approach': if (!this.attack) this.vx = this.facing * speed; break;
        case 'retreat':  if (!this.attack) this.vx = -this.facing * speed; break;
        case 'jump':
          if (this.onGround && !this.attack){ this.vy=-15; this.onGround=false; Audio.jump(); }
          if (!this.attack) this.vx = this.facing * speed*0.6;
          this.aiIntent='approach'; break;
        case 'block':
          if (this.onGround && !this.attack) this.state='block';
          break;
        case 'attack':
          if (adist < 100) this.startAttack(Math.random()<0.55?'poke':'swing');
          else if (adist < 300 && Math.random()<0.4) this.startAttack(Math.random()<0.6?'baguette':'bomb');
          else if (!this.attack) this.vx = this.facing * speed;
          break;
        case 'blast':
          if (this.meter>=100) this.startAttack('rocket'); else this.startAttack('baguette');
          this.aiIntent='approach'; break;
      }
    }

    update(foe){
      this.animT += 0.16;
      if (this.invuln>0) this.invuln--;
      if (this.cd.baguette>0) this.cd.baguette--;
      if (this.cd.bomb>0) this.cd.bomb--;
      // attack phase machine
      if (this.attack){
        this.attack.t++;
        const a = this.attack;
        if (a.type==='baguette' || a.type==='rocket' || a.type==='bomb'){
          if (a.t >= a.recover){ this.attack=null; if(this.state!=='hurt') this.state='idle'; }
        } else {
          if (a.phase==='startup' && a.t>=a.startup){ a.phase='active'; a.t=0; }
          else if (a.phase==='active' && a.t>=a.active){ a.phase='recover'; a.t=0; }
          else if (a.phase==='recover' && a.t>=a.recover){ this.attack=null; if(this.state!=='hurt') this.state='idle'; }
          // arm extension visual
          if (a.phase==='startup') this.armSwing = a.t/Math.max(1,a.startup)*0.5;
          else if (a.phase==='active') this.armSwing = 1;
          else this.armSwing = Math.max(0, 1 - a.t/Math.max(1,a.recover));
        }
      } else {
        this.armSwing *= 0.7;
      }

      if (this.state==='hurt'){ this.hurtTimer--; if (this.hurtTimer<=0 && this.onGround) this.state='idle'; }

      // physics
      this.x += this.vx;
      if (!this.attack && this.state!=='hurt') this.vx *= this.onGround ? 0.72 : 0.9;
      else this.vx *= 0.86;

      this.vy += GRAV;
      this.y += this.vy;
      if (this.y >= GROUND){ this.y = GROUND; this.vy = 0; if(!this.onGround){ this.onGround = true; if(this.state==='jump') this.state='idle'; } }
      else { this.onGround = false; if(this.state!=='hurt' && this.state!=='ko' && !this.attack) this.state='jump'; }

      // arena bounds
      this.x = Math.max(ARENA_L, Math.min(ARENA_R, this.x));

      // determine walk state
      if (this.onGround && !this.attack && this.state!=='hurt' && this.state!=='block' && this.state!=='ko'){
        this.state = Math.abs(this.vx) > 0.6 ? 'walk' : 'idle';
      }
      // passive meter gain
      this.meter = Math.min(100, this.meter + 0.06);
      this.stateTime++;
    }

    draw(g){
      const c = this.colors;
      const f = this.facing;
      g.save();
      g.translate(this.x, this.y);

      // ground shadow — wide soft halo + tight dark core, so warm fighters separate from the cobbles
      g.save();
      g.scale(1, .32);
      g.beginPath(); g.arc(0, 6/.32, this.w*0.5+13, 0, Math.PI*2); g.fillStyle='rgba(0,0,0,.32)'; g.fill();
      g.beginPath(); g.arc(0, 6/.32, this.w*0.42,    0, Math.PI*2); g.fillStyle='rgba(0,0,0,.6)';  g.fill();
      g.restore();

      g.scale(f, 1);   // face direction

      const hurtFlash = this.state==='hurt' && (Math.floor(this.stateTime/2)%2===0);
      const lean = this.state==='walk' ? Math.sin(this.animT)*3 : 0;
      const bob  = this.state==='idle' ? Math.sin(this.animT*0.8)*2 : 0;
      const koFall = this.state==='ko' ? Math.min(1,this.stateTime/26) : 0;

      g.save();
      g.translate(0, bob);
      if (koFall>0){ g.rotate(koFall*Math.PI*0.5*0.9); g.translate(0, koFall*10); }
      g.rotate(lean*Math.PI/180);

      // ===== Frenchman with beret, wielding a baguette =====
      const legPhase = this.state==='walk' ? Math.sin(this.animT) : 0;
      const armExt = this.armSwing;
      const skin='#e8b892', skinDk='#c98f68', trouser='#242a42', shoe='#141414';
      const shirt = hurtFlash ? '#ffffff' : '#f2efe6';
      const hipY=-46, shoY=-86, headR=14, headCY=-104;

      // ---- legs + shoes ----
      g.lineCap='round';
      g.strokeStyle=trouser; g.lineWidth=13;
      g.beginPath(); g.moveTo(-8,hipY); g.lineTo(-9 - legPhase*7, -3); g.stroke();
      g.beginPath(); g.moveTo( 8,hipY); g.lineTo( 9 + legPhase*7, -3); g.stroke();
      g.strokeStyle=shoe; g.lineWidth=7;
      g.beginPath(); g.moveTo(-9-legPhase*7,-2); g.lineTo(-14-legPhase*7,-1); g.stroke();
      g.beginPath(); g.moveTo( 9+legPhase*7,-2); g.lineTo( 14+legPhase*7,-1); g.stroke();

      // ---- back arm (behind torso) ----
      const backHy = shoY+12+22+Math.sin(this.animT)*2;
      g.strokeStyle=shirt; g.lineWidth=8; g.lineCap='round';
      g.beginPath(); g.moveTo(-this.w*0.16, shoY+12); g.lineTo(-this.w*0.16-6, backHy); g.stroke();
      g.fillStyle=skin; g.beginPath(); g.arc(-this.w*0.16-6, backHy, 4,0,Math.PI*2); g.fill();

      // ---- torso: Breton-striped shirt ----
      const tw=this.w*0.8, tx=-tw/2, ty=shoY, thh=hipY-shoY+6;
      g.save();
      g.shadowColor=c.glow; g.shadowBlur=hurtFlash?24:12;
      roundRectPath(g, tx, ty, tw, thh, 9); g.fillStyle=shirt; g.fill();
      g.shadowBlur=0;
      if (!hurtFlash){
        g.save(); roundRectPath(g, tx, ty, tw, thh, 9); g.clip();
        g.fillStyle=c.accent;
        for (let sy=ty+5; sy<ty+thh; sy+=10) g.fillRect(tx, sy, tw, 5);
        g.restore();
      }
      g.lineWidth=2.5; g.strokeStyle='rgba(0,0,0,.5)'; roundRectPath(g, tx, ty, tw, thh, 9); g.stroke();  // dark outline for floor separation
      g.restore();

      // ---- neckerchief ----
      g.fillStyle=c.beret;
      g.beginPath(); g.moveTo(-8, shoY+2); g.lineTo(8, shoY+2); g.lineTo(0, shoY+13); g.closePath(); g.fill();

      // ---- front arm + baguette weapon ----
      const sx=this.w*0.2, sy=shoY+10;
      let hx, hy, bang;
      if (this.state==='swing'){                 // overhead whack that sweeps down
        bang = -2.1 + armExt*2.7;
        hx = sx + 6 + Math.cos(bang)*6; hy = sy + 2 + Math.sin(bang)*6;
      } else if (this.state==='poke' || (this.attack && (this.attack.type==='baguette'||this.attack.type==='rocket'||this.attack.type==='bomb'))){
        hx = sx + 12 + armExt*26; hy = sy + 8; bang = -0.12;   // thrust / throw
      } else if (this.state==='block'){
        hx = sx - 2; hy = sy + 4; bang = -1.45;                // raised guard
      } else {
        hx = sx + 8 + Math.sin(this.animT)*2; hy = sy + 16; bang = -0.15;  // held ready
      }
      g.strokeStyle=shirt; g.lineWidth=9; g.lineCap='round';
      g.beginPath(); g.moveTo(sx, sy); g.lineTo(hx, hy); g.stroke();
      const hideBag = this.attack && this.attack.type==='baguette';   // baguette left the hand as a missile
      if (!hideBag) drawBaguetteWeapon(g, hx, hy, bang, 46, true);
      g.fillStyle=skin; g.beginPath(); g.arc(hx, hy, 5, 0, Math.PI*2); g.fill();

      // block shimmer
      if (this.state==='block'){
        g.strokeStyle='rgba(180,225,255,.85)'; g.lineWidth=3;
        g.beginPath(); g.arc(this.w*0.12, shoY+thh*0.4, this.w*0.55, -0.7, 0.7); g.stroke();
      }

      // ---- head ----
      g.save();
      g.translate(2, headCY);
      g.fillStyle=skin; g.fillRect(-4, headR-5, 8, 8);            // neck
      g.fillStyle = hurtFlash ? '#ffffff' : skin;                 // face
      g.beginPath(); g.arc(0,0,headR,0,Math.PI*2); g.fill();
      g.strokeStyle=skinDk; g.lineWidth=1.5; g.stroke();
      g.fillStyle=skin; g.beginPath(); g.arc(-headR+2, 2, 3, 0, Math.PI*2); g.fill();  // ear
      if (this.state==='ko'){
        g.strokeStyle='#20140a'; g.lineWidth=2;
        for (const ex of [4,10]){ g.beginPath(); g.moveTo(ex-2,-5); g.lineTo(ex+2,-1); g.moveTo(ex+2,-5); g.lineTo(ex-2,-1); g.stroke(); }
      } else {
        g.fillStyle='#20140a';
        g.beginPath(); g.arc(5,-3,2,0,Math.PI*2); g.fill();
        g.beginPath(); g.arc(11,-3,2,0,Math.PI*2); g.fill();
        g.strokeStyle='#20140a'; g.lineWidth=2.5;
        g.beginPath(); g.moveTo(1,-8); g.lineTo(8,-5); g.moveTo(8,-6); g.lineTo(14,-8); g.stroke();  // brows
      }
      // grand moustache
      g.strokeStyle='#3b2415'; g.lineWidth=3; g.lineCap='round';
      g.beginPath(); g.moveTo(1,6); g.quadraticCurveTo(5,3,9,5);
      g.moveTo(9,5); g.quadraticCurveTo(12,3,15,7); g.stroke();
      // mouth
      g.strokeStyle='#20140a'; g.lineWidth=2; g.beginPath();
      if (this.state==='hurt') g.arc(7, 9, 3.5, 0, Math.PI*2);
      else if (this.attack) g.arc(7, 9, 3, 0, Math.PI*2);
      else g.arc(7, 8, 4, 0.1*Math.PI, 0.9*Math.PI);
      g.stroke();
      // ---- beret (hat) ----
      g.fillStyle=c.beret;
      g.beginPath(); g.ellipse(1, -headR+3, headR*0.98, 5.5, 0, 0, Math.PI*2); g.fill();
      g.beginPath(); g.arc(1, -headR-2, headR*0.86, Math.PI, 0); g.fill();
      g.fillStyle='#1a1a1a'; g.beginPath(); g.arc(headR*0.55, -headR-3, 2.2, 0, Math.PI*2); g.fill();  // stalk
      g.restore();

      g.restore(); // bob/ko
      g.restore(); // translate/scale
    }
  }

  // draws a baguette starting at (x,y) extending length `len` along +x, rotated by `angle`
  function drawBaguetteWeapon(g, x, y, angle, len, glow){
    g.save();
    g.translate(x,y); g.rotate(angle);
    const bw = 9;
    g.shadowColor='#ffcf6b'; g.shadowBlur = glow?10:0;
    roundRectPath(g, 0, -bw/2, len, bw, bw/2);
    const grd = g.createLinearGradient(0,-bw/2,0,bw/2);
    grd.addColorStop(0,'#e9c079'); grd.addColorStop(.5,'#d9a441'); grd.addColorStop(1,'#a9761f');
    g.fillStyle = grd; g.fill();
    g.shadowBlur = 0;
    g.lineWidth = 1.5; g.strokeStyle = 'rgba(90,55,15,.65)'; g.stroke();
    // score marks
    g.strokeStyle = 'rgba(110,66,18,.8)'; g.lineWidth = 1.6;
    for (let i=1;i<4;i++){ const xx = len*i/4; g.beginPath(); g.moveTo(xx-3,-2); g.quadraticCurveTo(xx, -bw*0.6, xx+3,-1); g.stroke(); }
    // rounded tips highlight
    g.fillStyle='rgba(255,245,220,.35)'; g.beginPath(); g.ellipse(len-3,0,2.5,bw*0.28,0,0,Math.PI*2); g.fill();
    g.restore();
  }

  // rocket drawn pointing +x (caller flips by facing)
  function drawRocket(g, r){
    g.fillStyle='#e6e6ee'; roundRectPath(g, -r, -8, r*1.7, 16, 6); g.fill();
    g.lineWidth=1.5; g.strokeStyle='rgba(0,0,0,.25)'; roundRectPath(g, -r, -8, r*1.7, 16, 6); g.stroke();
    g.fillStyle='#ff4d4d'; g.beginPath(); g.moveTo(r*0.7,-8); g.lineTo(r*0.7+18,0); g.lineTo(r*0.7,8); g.closePath(); g.fill(); // nose
    g.fillStyle='#4d9dff';
    g.beginPath(); g.moveTo(-r,-8); g.lineTo(-r-9,-15); g.lineTo(-r+7,-8); g.closePath(); g.fill();   // fins
    g.beginPath(); g.moveTo(-r, 8); g.lineTo(-r-9, 15); g.lineTo(-r+7, 8); g.closePath(); g.fill();
    g.fillStyle='#22e0ff'; g.beginPath(); g.arc(r*0.05,0,4,0,Math.PI*2); g.fill();                    // window
  }
  // bomb drawn spinning; t drives the fuse-spark flicker
  function drawBomb(g, r, t){
    g.shadowColor='rgba(0,0,0,.5)'; g.shadowBlur=6;
    g.fillStyle='#141414'; g.beginPath(); g.arc(0,0,r*0.72,0,Math.PI*2); g.fill();
    g.shadowBlur=0;
    g.fillStyle='rgba(255,255,255,.18)'; g.beginPath(); g.arc(-r*0.22,-r*0.22,r*0.22,0,Math.PI*2); g.fill();
    g.fillStyle='#5a4a2a'; g.fillRect(-3,-r*0.82,6,5);                                                 // fuse cap
    g.strokeStyle='#8a5a2a'; g.lineWidth=2; g.beginPath(); g.moveTo(0,-r*0.82); g.quadraticCurveTo(7,-r-2,12,-r*0.72); g.stroke();
    g.fillStyle=(Math.floor(t/3)%2)?'#ffd166':'#ff6a3d'; g.shadowColor='#ffb347'; g.shadowBlur=8;
    g.beginPath(); g.arc(12,-r*0.72,3.2,0,Math.PI*2); g.fill(); g.shadowBlur=0;
  }

  function roundRectPath(g,x,y,w,h,r){
    r = Math.max(0, Math.min(r, w/2, h/2));    // never negative (thin cobble rows)
    g.beginPath();
    g.moveTo(x+r,y);
    g.arcTo(x+w,y,x+w,y+h,r);
    g.arcTo(x+w,y+h,x,y+h,r);
    g.arcTo(x,y+h,x,y,r);
    g.arcTo(x,y,x+w,y,r);
    g.closePath();
  }

  // ---------- Projectiles & particles ----------
  let blasts = [], sparks = [], hazards = [], hazardTimer = 140, shake = 0;
  function spawnProjectile(owner, kind){
    const f = owner.facing, base = { owner, kind, t:0, rot:0, hit:false };
    if (kind === 'rocket'){
      blasts.push({ ...base, x: owner.x + f*owner.w*0.5, y: owner.y - owner.h*0.55, vx: f*26, vy:0, grav:0, r:30, dmg:22, kb:18, life:80 });
    } else if (kind === 'bomb'){
      blasts.push({ ...base, x: owner.x + f*owner.w*0.4, y: owner.y - owner.h*0.9, vx: f*9, vy:-11, grav:0.45, spin:f*0.3, r:20, dmg:14, kb:14, life:220 });
    } else { // baguette missile (fast + big)
      blasts.push({ ...base, x: owner.x + f*owner.w*0.5, y: owner.y - owner.h*0.62, vx: f*19, vy:0, grav:0, r:34, dmg:16, kb:16, life:90 });
    }
  }
  function spawnSpark(x,y,color,n){
    for (let i=0;i<n;i++){
      const a = Math.random()*Math.PI*2, sp = 1+Math.random()*5;
      sparks.push({ x,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-2, life:20+Math.random()*16, color, r:2+Math.random()*3 });
    }
  }
  function updateProjectiles(f1,f2){
    for (const b of blasts){
      b.t++; b.x += b.vx;
      if (b.grav){ b.vy += b.grav; b.y += b.vy; }
      b.rot += (b.spin != null ? b.spin : 0.7*Math.sign(b.vx));
      b.life--;
      const foe = b.owner === f1 ? f2 : f1;
      const hb = foe.hurtbox();
      const overlap = (b.x+b.r > hb.x && b.x-b.r < hb.x+hb.w && b.y+b.r>hb.y && b.y-b.r<hb.y+hb.h);
      if (b.kind === 'bomb'){
        if (overlap || b.y >= GROUND-1 || b.life<=0){ explodeBomb(b, foe); b.life=0; }
      } else {
        if (overlap){ if (foe.takeHit(b.dmg * b.owner.powF, b.kb, Math.sign(b.vx))){ b.life=0; spawnSpark(b.x,b.y,'#ffb347',18); } }
        if (b.x < ARENA_L-60 || b.x > ARENA_R+60) b.life = 0;
      }
    }
    blasts = blasts.filter(b => b.life>0);
    for (const s of sparks){ s.x+=s.vx; s.y+=s.vy; s.vy+=0.25; s.life--; }
    sparks = sparks.filter(s => s.life>0);
  }
  function explodeBomb(b, foe){
    spawnSpark(b.x, b.y, '#ffd166', 26); spawnSpark(b.x, b.y, '#ff6a3d', 18); spawnSpark(b.x, b.y, '#ffffff', 8);
    shake = Math.min(22, shake + 11); Audio.boom();
    const d = Math.hypot(foe.x - b.x, (foe.y - foe.h*0.5) - b.y);
    if (d < 96) foe.takeHit(b.dmg * b.owner.powF * (1 - d/150), 15, Math.sign(foe.x - b.x || b.vx));
  }
  function drawProjectiles(g){
    for (const b of blasts){
      const c = b.owner.colors;
      g.save();
      if (b.kind === 'rocket'){
        g.fillStyle = '#ff9a3d';
        for (let i=1;i<=6;i++){ g.globalAlpha=0.32/i; g.beginPath(); g.arc(b.x-b.vx*i*0.7, b.y, 11-i, 0, Math.PI*2); g.fill(); }
        g.globalAlpha=1; g.translate(b.x, b.y); g.scale(Math.sign(b.vx), 1);
        drawRocket(g, b.r);
      } else if (b.kind === 'bomb'){
        g.translate(b.x, b.y);
        g.fillStyle='rgba(130,130,140,.4)';
        for (let i=1;i<=4;i++){ g.globalAlpha=0.32/i; g.beginPath(); g.arc(-b.vx*i*0.6, -b.vy*i*0.3, 7-i, 0, Math.PI*2); g.fill(); }
        g.globalAlpha=1; g.rotate(b.rot);
        drawBomb(g, b.r, b.t);
      } else {  // baguette missile
        g.fillStyle = c.glow;
        for (let i=1;i<=6;i++){ g.globalAlpha=0.30/i; g.beginPath(); g.arc(b.x-b.vx*i*0.9, b.y, 12-i, 0, Math.PI*2); g.fill(); }
        g.globalAlpha=1; g.translate(b.x, b.y); g.rotate(b.rot); g.scale(1.5, 1.5);
        drawBaguetteWeapon(g, -33, 0, 0, 66, true);
      }
      g.restore();
    }
    for (const s of sparks){
      g.globalAlpha = Math.max(0, s.life/26);
      g.fillStyle = s.color;
      g.beginPath(); g.arc(s.x,s.y,s.r,0,Math.PI*2); g.fill();
    }
    g.globalAlpha = 1;
  }

  // ---------- Flying food hazards (pies, french rolls, croissants) ----------
  function spawnHazard(){
    const r = Math.random();
    const type = r < 0.42 ? 'pie' : (r < 0.75 ? 'roll' : 'croissant');
    const fromLeft = Math.random() < 0.5;
    hazards.push({
      type,
      x: fromLeft ? ARENA_L - 40 : ARENA_R + 40,
      y: 110 + Math.random() * (GROUND - 210),
      vx: (fromLeft ? 1 : -1) * (type === 'roll' ? 8.5 + Math.random()*3 : 5.5 + Math.random()*2),
      vy: type === 'pie' ? -3 - Math.random()*2 : (Math.random()-0.5)*3,
      rot: 0, spin: (Math.random()-0.5) * 0.45,
      r: type === 'pie' ? 21 : type === 'croissant' ? 17 : 13,
      dmg: type === 'pie' ? 12 : type === 'roll' ? 8 : 10,
      kb:  type === 'pie' ? 12 : type === 'roll' ? 9 : 10,
      life: 220, hit: false
    });
  }
  function splat(h){
    spawnSpark(h.x, h.y, h.type === 'pie' ? '#fff6e0' : '#e8c37a', h.type === 'pie' ? 20 : 11);
    if (h.type === 'pie') spawnSpark(h.x, h.y, '#ffd9e6', 8);
    shake = Math.min(16, shake + 5);
  }
  function updateHazards(dt){
    if (state === 'fight'){
      hazardTimer -= dt;
      if (hazardTimer <= 0){
        spawnHazard();
        hazardTimer = 90 + Math.random()*130;
        Voice.hazard();
      }
    }
    for (const h of hazards){
      h.x += h.vx; h.y += h.vy; h.vy += 0.14; h.rot += h.spin; h.life--;
      if (!h.hit){
        for (const fter of [f1, f2]){
          const b = fter.hurtbox();
          if (h.x > b.x && h.x < b.x+b.w && h.y > b.y && h.y < b.y+b.h){
            h.hit = true;
            fter.takeHit(h.dmg, h.kb, Math.sign(h.vx) || 1);
            splat(h); h.life = 0; break;
          }
        }
      }
      if (h.life > 0 && h.y > GROUND + 2){ splat(h); h.life = 0; }
      if (h.x < ARENA_L - 90 || h.x > ARENA_R + 90) h.life = 0;
    }
    hazards = hazards.filter(h => h.life > 0);
  }
  function drawHazards(g){
    for (const h of hazards){
      g.save();
      g.translate(h.x, h.y); g.rotate(h.rot);
      g.shadowColor = 'rgba(255,220,150,.7)'; g.shadowBlur = 14;
      if (h.type === 'pie'){
        g.fillStyle = '#b98a4b'; g.beginPath(); g.arc(0,0,h.r,0,Math.PI*2); g.fill();       // tin
        g.fillStyle = '#fff6e0'; g.beginPath(); g.arc(0,-2,h.r*0.82,0,Math.PI*2); g.fill();  // cream
        g.fillStyle = '#ffd0dd'; g.beginPath(); g.arc(0,-3,h.r*0.34,0,Math.PI*2); g.fill();  // cherry
      } else if (h.type === 'croissant'){
        g.fillStyle = '#e0a94e'; g.strokeStyle = '#a9761f'; g.lineWidth = 2;
        g.beginPath();
        g.arc(0, 0, h.r, 0.18*Math.PI, 1.82*Math.PI);
        g.arc(5, 0, h.r*0.62, 1.82*Math.PI, 0.18*Math.PI, true);
        g.closePath(); g.fill(); g.stroke();
      } else { // french roll
        g.fillStyle = '#e8c37a'; g.strokeStyle = '#a9761f'; g.lineWidth = 2;
        g.beginPath(); g.ellipse(0,0,h.r,h.r*0.62,0,0,Math.PI*2); g.fill(); g.stroke();
        g.strokeStyle = 'rgba(120,72,20,.7)'; g.lineWidth = 1.5;
        g.beginPath(); g.moveTo(-h.r*0.55,0); g.lineTo(h.r*0.55,0); g.stroke();
      }
      g.restore();
    }
    g.shadowBlur = 0;
  }

  // ---------- Background (neon Paris) ----------
  const stars = Array.from({length:70}, () => ({ x:Math.random()*W, y:Math.random()*GROUND*0.8, r:Math.random()*1.4+0.3, p:Math.random()*Math.PI*2 }));
  let bgT = 0;
  function drawBackground(g){
    bgT += 0.01;
    // sky
    const sky = g.createLinearGradient(0,0,0,GROUND);
    sky.addColorStop(0,'#0a0620'); sky.addColorStop(0.5,'#1a0f38'); sky.addColorStop(1,'#3a1550');
    g.fillStyle = sky; g.fillRect(0,0,W,GROUND);

    // moon
    g.save(); g.shadowColor='#fff6d0'; g.shadowBlur=40;
    g.fillStyle='#f3ecc7'; g.beginPath(); g.arc(W-120,90,34,0,Math.PI*2); g.fill();
    g.restore();

    // stars
    for (const s of stars){
      const tw = 0.5+0.5*Math.sin(bgT*4+s.p);
      g.globalAlpha = 0.3+tw*0.7; g.fillStyle='#fff';
      g.beginPath(); g.arc(s.x,s.y,s.r,0,Math.PI*2); g.fill();
    }
    g.globalAlpha=1;

    // distant skyline silhouettes with neon windows
    drawSkyline(g);

    // Eiffel Tower (neon)
    drawEiffel(g, W*0.5, GROUND, 300);

    // neon grid floor
    drawFloor(g);
  }
  function drawSkyline(g){
    g.fillStyle='rgba(8,4,20,.85)';
    const base = GROUND;
    const blocks = [ [40,120,90],[150,80,140],[240,150,80],[720,110,120],[820,90,150],[900,70,110] ];
    for (const [x,w,h] of blocks){
      g.fillRect(x, base-h, w, h);
      // neon windows
      g.fillStyle = Math.random()>0 ? 'rgba(34,224,255,.25)' : '#22e0ff';
      for (let wy=base-h+12; wy<base-8; wy+=16)
        for (let wx=x+8; wx<x+w-6; wx+=14){
          g.globalAlpha = 0.12 + (Math.sin(wx*wy)+1)*0.18;
          g.fillStyle = (wx+wy)%2? '#22e0ff':'#ff3d9a';
          g.fillRect(wx,wy,5,7);
        }
      g.globalAlpha=1; g.fillStyle='rgba(8,4,20,.85)';
    }
  }
  function drawEiffel(g, cx, baseY, h){
    g.save();
    g.strokeStyle = '#ff3d9a';
    g.shadowColor = '#ff3d9a'; g.shadowBlur = 16;
    g.lineWidth = 3;
    const topY = baseY - h, topW = 8, baseW = 150;
    const wAt = (y) => { const t=(baseY-y)/h; return baseW*(1-t)*(1-t)*0.9 + topW; };
    // legs (curved)
    for (const dir of [-1,1]){
      g.beginPath();
      for (let y=baseY; y>=topY; y-=6){ const w=wAt(y)/2; g.lineTo(cx+dir*w, y); }
      g.stroke();
    }
    // horizontal platforms
    g.lineWidth=2;
    for (const py of [baseY-h*0.22, baseY-h*0.5, baseY-h*0.78]){
      const w = wAt(py)/2; g.beginPath(); g.moveTo(cx-w,py); g.lineTo(cx+w,py); g.stroke();
    }
    // cross lattice
    g.strokeStyle='rgba(34,224,255,.5)'; g.shadowColor='#22e0ff'; g.lineWidth=1;
    for (let y=baseY; y>topY; y-=22){
      const w1=wAt(y)/2, w2=wAt(y-22)/2;
      g.beginPath(); g.moveTo(cx-w1,y); g.lineTo(cx+w2,y-22);
      g.moveTo(cx+w1,y); g.lineTo(cx-w2,y-22); g.stroke();
    }
    // top spire + beacon
    g.strokeStyle='#ff3d9a'; g.shadowColor='#ff3d9a'; g.lineWidth=3;
    g.beginPath(); g.moveTo(cx, topY); g.lineTo(cx, topY-24); g.stroke();
    g.fillStyle='#fff'; g.shadowColor='#fff'; g.shadowBlur=20;
    g.beginPath(); g.arc(cx, topY-26, 3+Math.sin(bgT*6)*1.5, 0, Math.PI*2); g.fill();
    g.restore();
  }
  const CROISSANT_SPOTS = [
    [150, GROUND+34, 1.00, 0.35], [395, GROUND+16, 0.75, -0.25], [590, GROUND+64, 1.35, 0.50],
    [815, GROUND+28, 0.85, -0.50], [300, GROUND+92, 1.55, 0.15], [700, GROUND+96, 1.6, -0.3]
  ];
  function drawStreetCroissants(g){
    for (const [x,y,s,rot] of CROISSANT_SPOTS){
      g.save(); g.translate(x,y);
      g.fillStyle='rgba(0,0,0,.35)'; g.beginPath(); g.ellipse(0, 4*s, 15*s, 5*s, 0, 0, Math.PI*2); g.fill(); // shadow
      g.rotate(rot); g.scale(s,s);
      g.fillStyle='#e0a94e'; g.strokeStyle='#a9761f'; g.lineWidth=2;
      g.beginPath(); g.arc(0,0,13,0.18*Math.PI,1.82*Math.PI); g.arc(5,0,8,1.82*Math.PI,0.18*Math.PI,true); g.closePath(); g.fill(); g.stroke();
      g.strokeStyle='rgba(120,72,20,.5)'; g.lineWidth=1;
      for (let i=-1;i<=1;i++){ g.beginPath(); g.moveTo(i*5,-6); g.lineTo(i*5,6); g.stroke(); }
      g.fillStyle='rgba(255,245,220,.3)'; g.beginPath(); g.ellipse(-3,-4,4,2,0.4,0,Math.PI*2); g.fill(); // flour sheen
      g.restore();
    }
  }
  function drawFloor(g){
    // stone base
    const fg = g.createLinearGradient(0,GROUND,0,H);
    fg.addColorStop(0,'#2a2033'); fg.addColorStop(1,'#0b0712');
    g.fillStyle=fg; g.fillRect(0, GROUND, W, H-GROUND);

    // cobblestones, in perspective (rows grow toward the viewer)
    const rows=7, span=H-GROUND;
    for (let j=0;j<rows;j++){
      const t0=j/rows, t1=(j+1)/rows;
      const y0=GROUND + span*t0*t0;
      const y1=GROUND + span*t1*t1;
      const rh=y1-y0, sw=26 + t0*80, off=(j%2)*(sw*0.5);
      const pad=Math.min(2, rh*0.2), sh=Math.max(1, rh-pad*2), rr=Math.max(0, Math.min(10, rh*0.3));
      for (let x=-sw; x<W+sw; x+=sw){
        const cx=x+off;
        const h=(Math.sin(cx*0.21 + j*1.7)*0.5+0.5);            // stable per-stone shade
        const r=Math.round(30+h*14), gg=Math.round(26+h*12), bl=Math.round(46+h*20);  // cooler + darker so warm fighters pop
        roundRectPath(g, cx+2, y0+pad, sw-4, sh, rr);
        g.fillStyle='rgb('+r+','+gg+','+bl+')'; g.fill();
        g.lineWidth=1.4; g.strokeStyle='rgba(255,61,154,'+(0.05+t0*0.13).toFixed(3)+')'; g.stroke();  // neon rim
        g.strokeStyle='rgba(185,215,255,'+(0.04+t0*0.09).toFixed(3)+')'; g.lineWidth=1;
        g.beginPath(); g.moveTo(cx+5, y0+3); g.lineTo(cx+sw-6, y0+3); g.stroke();                     // top highlight
      }
    }
    // glowing kerb at the horizon
    g.save(); g.strokeStyle='rgba(255,61,154,.7)'; g.shadowColor='#ff3d9a'; g.shadowBlur=10; g.lineWidth=2;
    g.beginPath(); g.moveTo(0,GROUND); g.lineTo(W,GROUND); g.stroke(); g.restore();

    drawStreetCroissants(g);
  }

  // ---------- HUD ----------
  function drawHUD(g){
    // health bars
    drawBar(g, 30, 26, 380, f1, false, f1.colors);
    drawBar(g, W-30-380, 26, 380, f2, true, f2.colors);
    // meter bars
    drawMeter(g, 30, 60, 260, f1.meter, false, f1.colors);
    drawMeter(g, W-30-260, 60, 260, f2.meter, true, f2.colors);
    // round pips
    drawPips(g, 30, 78, f1.rounds, false);
    drawPips(g, W-30, 78, f2.rounds, true);
    // timer
    g.fillStyle='#fff'; g.font='700 30px Georgia'; g.textAlign='center';
    g.shadowColor='#ff3d9a'; g.shadowBlur=12;
    g.fillText(Math.ceil(roundTime).toString().padStart(2,'0'), W/2, 52);
    g.shadowBlur=0;
    g.font='12px Georgia'; g.fillStyle='#c9a961';
    g.fillText('ROUND '+roundNum, W/2, 70);
    // announce
    if (announce.text){
      const a = Math.min(1, announce.t/12) * Math.min(1, announce.life/16);
      const ay = H*0.42;
      g.textAlign='center';
      g.font = '800 '+announce.size+'px Georgia';
      const tw = g.measureText(announce.text).width;
      // pill backdrop so the callout pops off the neon Eiffel Tower behind it
      g.globalAlpha = a*0.6;
      g.fillStyle = 'rgba(8,4,20,1)';
      roundRectPath(g, W/2 - tw/2 - 22, ay - announce.size*0.82, tw+44, announce.size+18, 14); g.fill();
      // text
      g.globalAlpha = a;
      g.fillStyle = announce.color; g.shadowColor=announce.color; g.shadowBlur=24;
      g.fillText(announce.text, W/2, ay);
      g.shadowBlur=0; g.globalAlpha=1;
    }
    // mute indicator
    if (Audio.muted){ g.fillStyle='#8a8478'; g.font='12px Georgia'; g.textAlign='right'; g.fillText('🔇 muted (M)', W-30, H-16); }
    g.textAlign='left';
  }
  function drawBar(g,x,y,w,fighter,flip,c){
    const h=18;
    g.fillStyle='rgba(0,0,0,.5)'; g.fillRect(x-2,y-2,w+4,h+4);
    g.fillStyle='#2a0f22'; g.fillRect(x,y,w,h);
    const pct = Math.max(0,fighter.hp)/100;
    const bw = w*pct;
    const bx = flip ? x+w-bw : x;
    const grd = g.createLinearGradient(x,0,x+w,0);
    grd.addColorStop(0, pct>0.3?'#ff9a3d':'#ff3d3d'); grd.addColorStop(1, c.glow);
    g.fillStyle = grd; g.fillRect(bx, y, bw, h);
    g.strokeStyle='rgba(255,255,255,.3)'; g.lineWidth=1; g.strokeRect(x,y,w,h);
    // name
    g.fillStyle='#fff'; g.font='700 14px Georgia';
    g.textAlign = flip?'right':'left';
    g.fillText(fighter.name, flip?x+w:x, y-8);
    g.textAlign='left';
  }
  function drawMeter(g,x,y,w,val,flip,c){
    const h=8;
    g.fillStyle='rgba(0,0,0,.5)'; g.fillRect(x,y,w,h);
    const bw=w*val/100, bx=flip?x+w-bw:x;
    g.fillStyle = val>=100 ? '#fff' : c.accent;
    if (val>=100){ g.shadowColor=c.glow; g.shadowBlur=10; }
    g.fillRect(bx,y,bw,h); g.shadowBlur=0;
    if (val>=100){ g.save(); g.font='700 11px Georgia'; g.textAlign=flip?'right':'left';
      g.shadowColor='#000'; g.shadowBlur=4; g.fillStyle=c.accent;
      g.fillText('ROCKET READY', flip?x+w:x, y+19); g.restore(); g.textAlign='left'; }
  }
  function drawPips(g,x,y,n,flip){
    for (let i=0;i<2;i++){
      const px = flip ? x-9-i*22 : x+9+i*22;
      g.beginPath(); g.arc(px,y,8,0,Math.PI*2);
      if (i<n){ g.fillStyle='#ffd166'; g.shadowColor='#ffd166'; g.shadowBlur=10; g.fill(); g.shadowBlur=0; }
      else { g.fillStyle='rgba(255,255,255,.12)'; g.fill(); g.strokeStyle='rgba(255,255,255,.42)'; g.lineWidth=1.5; g.stroke(); }  // empty slot ring
    }
  }

  // ---------- Match / round flow ----------
  let f1, f2, mode='1p';
  let state = 'menu';            // menu | intro | fight | koPause | result
  let roundNum = 1, roundTime = 60, roundTimer=0;
  let announce = { text:'', color:'#fff', size:56, t:0, life:0 };
  let koDelay = 0;

  function setAnnounce(text,color='#fff',size=56,life=70){ announce={text,color,size,t:0,life}; }

  // ---- character select ----
  let p1Idx = 0, p2Idx = 1, p1Ready = false, p2Ready = false;

  function drawMini(g, c){
    g.clearRect(0,0,56,70);
    g.save(); g.translate(28, 64);           // feet near bottom
    const skin='#e8b892';
    // legs
    g.strokeStyle='#242a42'; g.lineWidth=6; g.lineCap='round';
    g.beginPath(); g.moveTo(-5,-20); g.lineTo(-6,-2); g.stroke();
    g.beginPath(); g.moveTo( 5,-20); g.lineTo( 6,-2); g.stroke();
    // striped torso
    const tw=20, th=22, tx=-tw/2, ty=-44;
    roundRectPath(g,tx,ty,tw,th,5); g.fillStyle='#f2efe6'; g.fill();
    g.save(); roundRectPath(g,tx,ty,tw,th,5); g.clip(); g.fillStyle=c.accent;
    for (let sy=ty+3; sy<ty+th; sy+=6) g.fillRect(tx,sy,tw,3); g.restore();
    g.lineWidth=1.4; g.strokeStyle='rgba(0,0,0,.25)'; roundRectPath(g,tx,ty,tw,th,5); g.stroke();
    // arm + held baguette
    g.strokeStyle='#f2efe6'; g.lineWidth=5; g.beginPath(); g.moveTo(6,-40); g.lineTo(13,-34); g.stroke();
    drawBaguetteWeapon(g, 13, -34, -0.15, 22, false);
    g.fillStyle=skin; g.beginPath(); g.arc(13,-34,3,0,Math.PI*2); g.fill();
    // head
    g.save(); g.translate(0,-50);
    g.fillStyle=skin; g.beginPath(); g.arc(0,0,9,0,Math.PI*2); g.fill();
    g.strokeStyle='#3b2415'; g.lineWidth=2; g.lineCap='round';
    g.beginPath(); g.moveTo(-4,3); g.quadraticCurveTo(-1,1,0,3); g.moveTo(0,3); g.quadraticCurveTo(1,1,4,3); g.stroke();  // moustache
    g.fillStyle='#20140a'; g.beginPath(); g.arc(-3,-1,1.4,0,7); g.fill(); g.beginPath(); g.arc(3,-1,1.4,0,7); g.fill();
    g.fillStyle=c.beret; g.beginPath(); g.ellipse(0,-8,10,3.5,0,0,Math.PI*2); g.fill();
    g.beginPath(); g.arc(1,-11,8,Math.PI,0); g.fill();
    g.restore();
    g.restore();
  }

  function buildRoster(){
    const r = document.getElementById('roster'); r.innerHTML = '';
    ROSTER.forEach((c, i) => {
      const card = document.createElement('div');
      card.className = 'card'; card.dataset.idx = i;
      const cv = document.createElement('canvas'); cv.width = 112; cv.height = 140;  // 2x, drawn in 56x70 space
      const cvx = cv.getContext('2d'); cvx.scale(2,2); drawMini(cvx, c.colors); card.appendChild(cv);
      const nm = document.createElement('div'); nm.className = 'cname'; nm.textContent = c.name; card.appendChild(nm);
      [['SPD',c.spd],['PWR',c.pow],['DEF',c.def]].forEach(([lbl,v]) => {
        const s = document.createElement('div'); s.className = 'stat';
        s.innerHTML = '<b>'+lbl+'</b><span class="bar '+lbl.toLowerCase()+'"><i style="width:'+(v/5*100)+'%"></i></span>';
        card.appendChild(s);
      });
      const r1 = document.createElement('div'); r1.className='rdy r1'; r1.textContent='P1'; r1.style.display='none'; card.appendChild(r1);
      const r2 = document.createElement('div'); r2.className='rdy r2'; r2.textContent='P2'; r2.style.display='none'; card.appendChild(r2);
      card.addEventListener('click', () => { p1Idx = i; p1Ready = false; if (mode!=='2p') p2Ready = false; Audio.poke(); renderSelect(); });
      r.appendChild(card);
    });
  }

  function renderSelect(){
    const cards = [...document.querySelectorAll('#roster .card')];
    cards.forEach((card,i) => {
      card.classList.toggle('p1cursor', i===p1Idx);
      card.classList.toggle('p2cursor', i===p2Idx);
      card.querySelector('.rdy.r1').style.display = (i===p1Idx && p1Ready) ? 'block' : 'none';
      card.querySelector('.rdy.r2').style.display = (i===p2Idx && p2Ready) ? 'block' : 'none';
    });
    document.getElementById('p1tag').textContent = 'P1: ' + ROSTER[p1Idx].name + (p1Ready?' ✔':'');
    document.getElementById('p2tag').textContent = (mode==='2p'?'P2: ':'CPU: ') + ROSTER[p2Idx].name + (p2Ready?' ✔':'');
    const b = document.getElementById('btnStart');
    b.disabled = false; b.textContent = 'START FIGHT ▶';   // always launchable with current picks (mobile-friendly)
    document.getElementById('selHint').innerHTML = mode==='2p'
      ? 'P1 <span class="key">A</span><span class="key">D</span> move · <span class="key">F</span> ready &nbsp;·&nbsp; P2 <span class="key">←</span><span class="key">→</span> move · <span class="key">,</span> ready'
      : 'P1 <span class="key">A</span><span class="key">D</span> move · <span class="key">F</span> ready · CPU auto-picks · click a card to choose';
  }

  function openSelect(m){
    mode = m; state = 'select';
    p1Idx = 0; p2Idx = 1; p1Ready = false; p2Ready = false;
    hideScreens(); document.getElementById('select').classList.remove('hidden');
    Audio.resume(); renderSelect();
  }
  function selMove(player, dir){
    if (player===1){ p1Idx = (p1Idx+dir+ROSTER.length)%ROSTER.length; p1Ready=false; if(mode!=='2p') p2Ready=false; }
    else { p2Idx = (p2Idx+dir+ROSTER.length)%ROSTER.length; p2Ready=false; }
    Audio.poke(); renderSelect();
  }
  function selReady(player){
    if (player===1){
      p1Ready = !p1Ready;
      if (mode!=='2p'){                       // CPU picks a random fighter when P1 readies
        if (p1Ready){ p2Idx = Math.floor(Math.random()*ROSTER.length); p2Ready = true; }
        else p2Ready = false;
      }
    } else if (mode==='2p'){ p2Ready = !p2Ready; }
    Audio.bell(); renderSelect();
  }
  function beginSelectedMatch(){
    p1Ready = true; p2Ready = true;                // launch with whatever is currently selected
    p1map = (mode==='2p') ? P1MAP_2P : P1MAP_1P;   // 1P: P1 also gets arrows + space/⌘
    f1 = new Fighter(W*0.32,'p1', ROSTER[p1Idx], true);
    f2 = new Fighter(W*0.68,'p2', ROSTER[p2Idx], mode==='2p');
    f1.rounds=0; f2.rounds=0; roundNum=1;
    hideScreens(); Audio.resume();
    startRound();
  }
  function startRound(){
    f1.reset(); f2.reset();
    f1.x=W*0.32; f2.x=W*0.68; f1.facing=1; f2.facing=-1;
    roundTime=60; roundTimer=0; blasts=[]; sparks=[]; hazards=[]; hazardTimer=150; koDelay=0;
    state='intro'; introT=0;
    for (const kk in keys) if (kk.startsWith('_pressed_')) keys[kk] = false;  // drop stale edge triggers from select
    setAnnounce('ROUND '+roundNum,'#c9a961',56,10);
    Voice.round();
  }
  let introT=0;
  function beginFight(){ state='fight'; setAnnounce('FIGHT!','#ffe566',64,40); Audio.bell(); Voice.fight(); }

  function endRound(winner){
    if (state!=='fight') return;
    state='koPause'; koDelay=0;
    if (winner==='draw'){ setAnnounce('DOUBLE K.O.!','#fff',56,120); }
    else {
      const wf = winner===f1?f1:f2; wf.rounds++;
      Audio.ko(); Voice.ko();
      setAnnounce('K.O.','#ff3d3d',88,120);
    }
  }
  function afterKO(){
    if (f1.rounds>=2 || f2.rounds>=2 || (roundNum>=3)){
      // match over
      let champ = f1.rounds>f2.rounds ? f1 : (f2.rounds>f1.rounds ? f2 : null);
      showResult(champ);
    } else {
      roundNum++;
      startRound();
    }
  }

  // ---------- Screens ----------
  const $ = id => document.getElementById(id);
  function hideScreens(){ ['menu','result','pause','select'].forEach(id=>$(id).classList.add('hidden')); }
  function showResult(champ){
    state='result';
    if (champ){
      $('resultTitle').textContent = champ.name + ' WINS';
      $('resultSub').innerHTML = (champ.side==='p1'?'🥖 ':'🥖 ') +
        ['Magnifique!','Formidable!','Très bien!','Champion de Paris!'][Math.floor(Math.random()*4)];
      Voice.win();
    } else {
      $('resultTitle').textContent = 'DRAW';
      $('resultSub').textContent = 'Both loaves crumbled.';
    }
    $('result').classList.remove('hidden');
  }
  let paused=false;
  function togglePause(){
    if (state==='menu'||state==='result'||state==='select') return;
    paused = !paused;
    $('pause').classList.toggle('hidden', !paused);
  }

  $('btn1p').onclick = () => openSelect('1p');
  $('btn2p').onclick = () => openSelect('2p');
  $('btnStart').onclick = () => beginSelectedMatch();
  $('btnSelBack').onclick = () => { state='menu'; hideScreens(); $('menu').classList.remove('hidden'); };
  $('btnRematch').onclick = () => beginSelectedMatch();                 // same fighters
  $('btnMenu').onclick = () => { state='menu'; hideScreens(); $('menu').classList.remove('hidden'); };
  $('btnResume').onclick = () => togglePause();
  $('btnQuit').onclick = () => { paused=false; state='menu'; hideScreens(); $('menu').classList.remove('hidden'); };

  // ---------- Combat resolution ----------
  function resolveHits(atk, def){
    if (!atk.attack || atk.attack.hit) return;
    const hb = atk.hitbox(); if (!hb) return;
    const db = def.hurtbox();
    if (hb.x < db.x+db.w && hb.x+hb.w > db.x && hb.y < db.y+db.h && hb.y+hb.h > db.y){
      atk.attack.hit = true;
      const dir = Math.sign(atk.facing);
      const dmg = atk.attack.dmg * atk.powF, kb = atk.attack.kb;
      def.takeHit(dmg, kb, dir);
      atk.meter = Math.min(100, atk.meter + atk.attack.dmg*0.6);
    }
  }

  // ---------- Main loop ----------
  let last=performance.now();
  function loop(now){
    const dt = Math.min(2, (now-last)/16.67); last=now;
    requestAnimationFrame(loop);
    if (!paused) step(dt);
    render();
  }

  function step(dt){
    // announce timers
    if (announce.life>0){ announce.t+=dt*2; announce.life-=dt; }

    if (state==='intro'){
      introT += dt;
      // idle physics so they stand
      f1.update(f2); f2.update(f1);
      if (introT > 46) beginFight();
      return;
    }

    if (state==='fight'){
      roundTimer += dt; roundTime = Math.max(0, 60 - roundTimer/60*60*0);  // handled below
      roundTime = Math.max(0, 60 - roundTimer/60);
      // control
      if (f1.human) f1.controlHuman(p1map); else f1.controlAI(f2);
      if (f2.human) f2.controlHuman(P2MAP); else f2.controlAI(f1);
      f1.update(f2); f2.update(f1);
      resolveHits(f1,f2); resolveHits(f2,f1);
      updateProjectiles(f1,f2);
      updateHazards(dt);

      // separation so loaves don't overlap fully
      const overlap = (f1.w/2+f2.w/2) - Math.abs(f1.x-f2.x);
      if (overlap>0){ const push=overlap/2*Math.sign(f1.x-f2.x||1); f1.x+=push; f2.x-=push;
        f1.x=Math.max(ARENA_L,Math.min(ARENA_R,f1.x)); f2.x=Math.max(ARENA_L,Math.min(ARENA_R,f2.x)); }

      // win checks
      if (!f1.alive && !f2.alive) endRound('draw');
      else if (!f1.alive) endRound(f2);
      else if (!f2.alive) endRound(f1);
      else if (roundTime<=0){
        // time out -> higher hp wins
        if (f1.hp>f2.hp) endRound(f1); else if (f2.hp>f1.hp) endRound(f2); else endRound('draw');
      }
    }
    else if (state==='koPause'){
      koDelay += dt;
      f1.update(f2); f2.update(f1);
      updateProjectiles(f1,f2);
      updateHazards(dt);
      if (koDelay>70) afterKO();
    }

    if (shake>0) shake=Math.max(0, shake-dt*1.2);
  }

  function render(){
    ctx.save();
    if (shake>0){ ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake); }
    drawBackground(ctx);

    if (state!=='menu' && f1 && f2){
      // draw order: farther-back fighter first (by facing not needed) — draw both
      drawProjectiles(ctx);
      // draw the one behind first for slight depth: draw lower hp? just draw p2 then p1
      f2.draw(ctx); f1.draw(ctx);
      drawHazards(ctx);
      drawProjectiles(ctx); // sparks on top
      drawHUD(ctx);
    }
    ctx.restore();

    // subtle scanline / vignette
    const vg = ctx.createRadialGradient(W/2,H/2,H*0.4,W/2,H/2,H*0.9);
    vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,.45)');
    ctx.fillStyle=vg; ctx.fillRect(0,0,W,H);

    updateTouchVisibility();
  }

  // ---------- Mobile touch controls (drive Player 1) ----------
  const touchEl = document.getElementById('touch');
  let touchEnabled = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
  const TOUCH_KEYS = ['a','d','w','s','f','g','h','r','b'];
  let touchWasActive = false;
  function updateTouchVisibility(){
    const active = touchEnabled && (state==='fight' || state==='intro' || state==='koPause');
    if (active !== touchWasActive){
      touchEl.classList.toggle('on', active);
      if (!active) TOUCH_KEYS.forEach(k => { keys[k]=false; });   // release any held buttons on leave
      touchWasActive = active;
    }
  }
  function setupTouch(){
    if (touchEnabled) document.body.classList.add('touch');
    // some devices only reveal touch after the first touch
    window.addEventListener('touchstart', () => { touchEnabled = true; document.body.classList.add('touch'); }, { once:true, passive:true });
    const press   = k => { if(!keys[k]) keys['_pressed_'+k]=true; keys[k]=true; };
    const release = k => { keys[k]=false; };
    touchEl.querySelectorAll('.tbtn').forEach(btn => {
      const k = btn.dataset.k;
      const on  = e => { e.preventDefault(); press(k); };
      const off = e => { e.preventDefault(); release(k); };
      btn.addEventListener('touchstart', on,  { passive:false });
      btn.addEventListener('touchend',   off, { passive:false });
      btn.addEventListener('touchcancel',off, { passive:false });
      btn.addEventListener('mousedown',  on);   // desktop testing
      btn.addEventListener('mouseup',    off);
      btn.addEventListener('mouseleave', off);
    });
  }
  setupTouch();

  buildRoster();
  // tiny test hook (only when the URL ends in #debug) — no effect on normal play
  if (location.hash === '#debug') window.__bb = { get f1(){ return f1; }, get f2(){ return f2; }, get state(){ return state; }, get blasts(){ return blasts; } };
  // La Marseillaise on load — try immediately; browsers gate autoplay, so also fire on the first interaction
  Audio.startAnthem();
  const kickAnthem = () => Audio.startAnthem();
  window.addEventListener('pointerdown', kickAnthem, { once:true });
  window.addEventListener('keydown', kickAnthem, { once:true });
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>