// V3 — Special bubbles (Tesla/Ember/Champagne), chain combos, ENCORE letters

const { useState, useEffect, useRef, useCallback } = React;

// Letters spelling ENCORE — collect all 6 in a single life for an extra heart
const ENCORE_LETTERS = ['E','N','C','O','R','E'];

// Special bubble specs — directional pop creates an effect
const SPECIAL_BUBBLES = {
  tesla:     { color: '#9bd5ff', glow: 'rgba(155,213,255,0.8)' },     // electric — arcs sideways
  ember:     { color: '#ff8a3a', glow: 'rgba(255,138,58,0.8)' },      // fire — drops flame on floor
  champagne: { color: '#f5e8c8', glow: 'rgba(245,232,200,0.8)' },     // bubbly — flood + clear screen
};

// ============ ROOM DEFINITIONS ============
// Each room: title, platforms array, enemy spawns, optional theme.
// theme = { wind: -2|0|2, fog: bool, lowGrav: bool, falling: bool, locked: bool, color: '...' }
const ROOMS = [
  {
    title: 'Below 42nd Street',
    subtitle: 'a forgotten subway tunnel',
    theme: { color: 'subway' },
    platforms: [
      { x: 24, y: 200, w: 280, h: 24 },
      { x: 816, y: 200, w: 280, h: 24 },
      { x: 380, y: 320, w: 360, h: 24 },
      { x: 24, y: 460, w: 320, h: 24 },
      { x: 776, y: 460, w: 320, h: 24 },
    ],
    enemies: [
      { x: 200, y: 160, variant: 0 },
      { x: 920, y: 160, variant: 1 },
      { x: 540, y: 280, variant: 0 },
    ],
  },
  {
    title: 'The Speakeasy Stairs',
    subtitle: 'mind the velvet rope',
    theme: { color: 'speakeasy' },
    platforms: [
      { x: 24, y: 180, w: 220, h: 24 },
      { x: 880, y: 180, w: 216, h: 24 },
      { x: 240, y: 300, w: 220, h: 24 },
      { x: 660, y: 300, w: 220, h: 24 },
      { x: 440, y: 420, w: 240, h: 24 },
      { x: 24, y: 540, w: 320, h: 24 },
      { x: 776, y: 540, w: 320, h: 24 },
    ],
    enemies: [
      { x: 120, y: 140, variant: 1 },
      { x: 980, y: 140, variant: 0 },
      { x: 340, y: 260, variant: 0 },
      { x: 760, y: 260, variant: 1 },
    ],
  },
  {
    title: 'The Boiler Room',
    subtitle: 'mind the steam',
    theme: { color: 'boiler', steam: true },
    platforms: [
      { x: 24, y: 240, w: 220, h: 24 },
      { x: 460, y: 220, w: 200, h: 24 },
      { x: 876, y: 240, w: 220, h: 24 },
      { x: 200, y: 400, w: 280, h: 24 },
      { x: 640, y: 400, w: 280, h: 24 },
      { x: 440, y: 560, w: 240, h: 24 },
    ],
    enemies: [
      { x: 120, y: 200, variant: 0 },
      { x: 540, y: 180, variant: 1 },
      { x: 960, y: 200, variant: 0 },
      { x: 320, y: 360, variant: 1 },
      { x: 760, y: 360, variant: 0 },
    ],
  },
  {
    title: 'The Penthouse',
    subtitle: 'high above the gloom',
    theme: { color: 'penthouse', lowGrav: true },
    platforms: [
      { x: 24, y: 120, w: 200, h: 24 },
      { x: 896, y: 120, w: 200, h: 24 },
      { x: 460, y: 200, w: 200, h: 24 },
      { x: 200, y: 320, w: 200, h: 24 },
      { x: 720, y: 320, w: 200, h: 24 },
      { x: 460, y: 440, w: 200, h: 24 },
      { x: 24, y: 560, w: 280, h: 24 },
      { x: 816, y: 560, w: 280, h: 24 },
    ],
    enemies: [
      { x: 100, y: 80, variant: 1 },
      { x: 980, y: 80, variant: 0 },
      { x: 540, y: 160, variant: 1 },
      { x: 280, y: 280, variant: 0 },
      { x: 800, y: 280, variant: 1 },
      { x: 540, y: 400, variant: 0 },
    ],
  },
  {
    title: 'The Rooftop',
    subtitle: 'where the wind howls',
    theme: { color: 'rooftop', wind: 1.2 },
    platforms: [
      { x: 24, y: 200, w: 240, h: 24 },
      { x: 380, y: 280, w: 160, h: 24 },
      { x: 600, y: 360, w: 160, h: 24 },
      { x: 820, y: 280, w: 160, h: 24 },
      { x: 1024, y: 200, w: 72, h: 24 },
      { x: 200, y: 460, w: 200, h: 24 },
      { x: 720, y: 460, w: 200, h: 24 },
      { x: 460, y: 560, w: 200, h: 24 },
    ],
    enemies: [
      { x: 100, y: 160, variant: 0 },
      { x: 440, y: 240, variant: 1 },
      { x: 880, y: 240, variant: 0 },
      { x: 280, y: 420, variant: 1 },
      { x: 800, y: 420, variant: 0 },
      { x: 540, y: 520, variant: 1 },
    ],
  },
  {
    title: 'The Marquee',
    subtitle: 'one-way through neon',
    theme: { color: 'marquee', oneWay: true },
    platforms: [
      { x: 24, y: 180, w: 1072, h: 24, oneWay: true },
      { x: 200, y: 320, w: 220, h: 24, oneWay: true },
      { x: 700, y: 320, w: 220, h: 24, oneWay: true },
      { x: 440, y: 440, w: 240, h: 24, oneWay: true },
      { x: 24, y: 560, w: 320, h: 24 },
      { x: 776, y: 560, w: 320, h: 24 },
    ],
    enemies: [
      { x: 200, y: 140, variant: 1 },
      { x: 880, y: 140, variant: 0 },
      { x: 280, y: 280, variant: 0 },
      { x: 800, y: 280, variant: 1 },
      { x: 540, y: 400, variant: 0 },
      { x: 200, y: 520, variant: 1 },
      { x: 880, y: 520, variant: 0 },
    ],
  },
  {
    title: 'The Backstage Catwalks',
    subtitle: 'narrow paths · falling props',
    theme: { color: 'backstage', falling: true },
    platforms: [
      { x: 60, y: 180, w: 140, h: 24 },
      { x: 280, y: 240, w: 140, h: 24 },
      { x: 500, y: 180, w: 140, h: 24 },
      { x: 720, y: 240, w: 140, h: 24 },
      { x: 920, y: 180, w: 140, h: 24 },
      { x: 180, y: 380, w: 140, h: 24 },
      { x: 400, y: 440, w: 140, h: 24 },
      { x: 620, y: 380, w: 140, h: 24 },
      { x: 840, y: 440, w: 140, h: 24 },
      { x: 60, y: 580, w: 220, h: 24 },
      { x: 840, y: 580, w: 220, h: 24 },
    ],
    enemies: [
      { x: 120, y: 140, variant: 0 },
      { x: 580, y: 140, variant: 1 },
      { x: 980, y: 140, variant: 0 },
      { x: 340, y: 200, variant: 1 },
      { x: 780, y: 200, variant: 0 },
      { x: 240, y: 340, variant: 1 },
      { x: 460, y: 400, variant: 0 },
      { x: 680, y: 340, variant: 1 },
    ],
  },
  {
    title: 'The Hall of Mirrors',
    subtitle: 'every reflection is a witness',
    theme: { color: 'mirrors', mirror: true },
    platforms: [
      { x: 24, y: 240, w: 1072, h: 24 },
      { x: 24, y: 460, w: 1072, h: 24 },
      { x: 240, y: 350, w: 200, h: 24 },
      { x: 680, y: 350, w: 200, h: 24 },
      { x: 480, y: 130, w: 160, h: 24 },
    ],
    enemies: [
      { x: 100, y: 200, variant: 0 },
      { x: 980, y: 200, variant: 0 },
      { x: 320, y: 310, variant: 1 },
      { x: 760, y: 310, variant: 1 },
      { x: 100, y: 420, variant: 1 },
      { x: 980, y: 420, variant: 1 },
      { x: 540, y: 90, variant: 0 },
    ],
  },
  {
    title: 'The Brass Cage',
    subtitle: 'the final reel',
    theme: { color: 'cage', boss: true },
    platforms: [
      { x: 24, y: 220, w: 1072, h: 24 },
      { x: 24, y: 380, w: 380, h: 24 },
      { x: 716, y: 380, w: 380, h: 24 },
      { x: 380, y: 520, w: 360, h: 24 },
    ],
    enemies: [
      { x: 200, y: 180, variant: 0 },
      { x: 920, y: 180, variant: 1 },
      { x: 200, y: 340, variant: 1 },
      { x: 880, y: 340, variant: 0 },
      { x: 540, y: 480, variant: 0 },
      { x: 100, y: 480, variant: 1 },
      { x: 1000, y: 480, variant: 1 },
    ],
  },
];

const FRUITS = [
  { name: 'cherry', color: '#a02828', leaf: '#3a5a20', value: 100 },
  { name: 'lemon', color: '#e8c170', leaf: '#3a5a20', value: 200 },
  { name: 'plum', color: '#6b1a4a', leaf: '#3a5a20', value: 300 },
];

const PHYS_V2 = {
  gravity: 0.65,
  jumpVel: -13.5,
  moveAcc: 0.7,
  maxRun: 4.8,
  friction: 0.78,
  airFriction: 0.94,
  bubbleSpeed: 6,
  bubbleLife: 360, // frames before bubble drifts up & pops
  bubbleFloat: -0.45, // upward drift per frame after settling
  enemySpeed: 0.7,
  cooldown: 18, // frames between blows
};

const ROOM_W = 1120;
const ROOM_H = 720;

// ============ SVG SPRITES ============

const FlapperHeroV2 = ({ running, jumping, blowing, frame, facing }) => {
  const legSwing = jumping ? 0 : (running ? [0, 1, 0, -1][frame] : 0);
  const armUp = blowing ? -50 : (jumping ? -8 : (running ? [-1, 1, -1, 1][frame] * 4 : 0));

  return (
    <svg viewBox="0 0 36 56" width="36" height="56" style={{
      overflow: 'visible',
      transform: facing < 0 ? 'scaleX(-1)' : 'none',
    }}>
      <ellipse cx="18" cy="55" rx="11" ry="2" fill="#000" opacity="0.5" />
      <g transform={`translate(${legSwing}, 0)`}>
        <path d={`M 14 38 L ${13 - legSwing} 52 L ${15 - legSwing} 52 L 16 38 Z`} fill="#ead8b1" />
        <path d={`M 20 38 L ${21 + legSwing} 52 L ${23 + legSwing} 52 L 22 38 Z`} fill="#ead8b1" />
        <ellipse cx={14 - legSwing} cy="53" rx="3.2" ry="1.5" fill="#0a0908" />
        <ellipse cx={22 + legSwing} cy="53" rx="3.2" ry="1.5" fill="#0a0908" />
      </g>
      <path d="M 10 22 L 8 38 L 28 38 L 26 22 Z" fill="#0a0908" />
      <rect x="8" y="34" width="20" height="2.5" fill="#d4a84b" />
      {[...Array(11)].map((_, i) => (
        <g key={i} transform={`translate(${8 + i * 2}, 37)`}>
          <line x1="0" y1="0" x2="0" y2={3 + (i % 2)} stroke="#d4a84b" strokeWidth="0.5" />
          <circle cx="0" cy={3 + (i % 2)} r="0.7" fill="#e8c170" />
        </g>
      ))}
      <path d="M 12 26 L 18 30 L 24 26" stroke="#d4a84b" strokeWidth="0.6" fill="none" opacity="0.7" />
      <path d="M 11 18 L 10 24 L 26 24 L 25 18 Z" fill="#0a0908" />

      {/* Blowing arm — held up to face */}
      <g transform={`rotate(${armUp} 25 20)`}>
        <path d="M 26 19 L 29 30 L 27 31 L 24 20 Z" fill="#ead8b1" />
      </g>
      <g transform={`rotate(${-armUp * 0.3} 11 20)`}>
        <path d="M 10 19 L 7 30 L 9 31 L 12 20 Z" fill="#ead8b1" />
      </g>

      <rect x="16" y="14" width="4" height="5" fill="#ead8b1" />
      {[...Array(5)].map((_, i) => (
        <circle key={i} cx={14 + i * 2} cy={17 + Math.abs(i - 2) * 0.6} r="0.7" fill="#f5e8c8" />
      ))}
      <ellipse cx="18" cy="11" rx="6" ry="6.5" fill="#ead8b1" />
      <path d="M 11 11 Q 11 4 18 4 Q 25 4 25 11 L 25 13 Q 22 11 18 11 Q 14 11 11 13 Z" fill="#0a0908" />
      <path d="M 13 8 Q 18 6 23 8 L 23 10 Q 18 8.5 13 10 Z" fill="#0a0908" />
      <path d="M 11 7 Q 18 5 25 7 L 25 8.5 Q 18 6.5 11 8.5 Z" fill="#d4a84b" />
      <circle cx="14" cy="7" r="0.8" fill="#e8c170" />
      <path d="M 13 7 Q 9 2 7 -2 Q 9 -1 11 1 Q 12 4 13 7" fill="#d4a84b" opacity="0.85" />
      <ellipse cx="20" cy="11.5" rx="1.2" ry="0.6" fill="#0a0908" />
      <ellipse cx="15" cy="11.5" rx="1.2" ry="0.6" fill="#0a0908" />
      {/* Blowing lips — pursed */}
      {blowing
        ? <ellipse cx="22" cy="14" rx="1.6" ry="1.2" fill="#6b1a1a" stroke="#a02828" strokeWidth="0.3" />
        : <ellipse cx="18" cy="14" rx="1.4" ry="0.7" fill="#6b1a1a" />
      }
    </svg>
  );
};

const FruitSprite = ({ kind }) => {
  if (kind === 'cherry') {
    return (
      <svg viewBox="0 0 26 26" width="26" height="26">
        <path d="M 8 6 Q 13 0 18 6" stroke="#3a5a20" strokeWidth="1.5" fill="none" />
        <path d="M 13 4 Q 16 -2 22 -2" stroke="#3a5a20" strokeWidth="0.8" fill="none" />
        <ellipse cx="22" cy="-2" rx="3" ry="1.5" fill="#3a5a20" transform="rotate(20 22 -2)" />
        <circle cx="8" cy="14" r="6" fill="#a02828" stroke="#6b1a1a" strokeWidth="0.5" />
        <circle cx="6" cy="12" r="1.4" fill="#e8a070" opacity="0.7" />
        <circle cx="18" cy="14" r="6" fill="#a02828" stroke="#6b1a1a" strokeWidth="0.5" />
        <circle cx="16" cy="12" r="1.4" fill="#e8a070" opacity="0.7" />
      </svg>
    );
  }
  if (kind === 'lemon') {
    return (
      <svg viewBox="0 0 26 26" width="26" height="26">
        <ellipse cx="13" cy="13" rx="11" ry="8" fill="#e8c170" stroke="#8a6a1f" strokeWidth="0.6" transform="rotate(-15 13 13)" />
        <path d="M 2 8 L -1 5" stroke="#8a6a1f" strokeWidth="0.8" />
        <path d="M 24 18 L 27 21" stroke="#8a6a1f" strokeWidth="0.8" />
        <ellipse cx="9" cy="10" rx="3" ry="1.5" fill="#f5e8c8" opacity="0.7" />
      </svg>
    );
  }
  // plum
  return (
    <svg viewBox="0 0 26 26" width="26" height="26">
      <circle cx="13" cy="14" r="10" fill="#6b1a4a" stroke="#3a0a28" strokeWidth="0.6" />
      <path d="M 13 4 Q 13 14 13 24" stroke="#3a0a28" strokeWidth="0.6" fill="none" />
      <ellipse cx="9" cy="10" rx="2.5" ry="1.5" fill="#a04080" opacity="0.6" />
      <path d="M 13 4 Q 16 0 20 -1" stroke="#3a5a20" strokeWidth="0.8" fill="none" />
      <ellipse cx="20" cy="-1" rx="2.5" ry="1" fill="#3a5a20" transform="rotate(20 20 -1)" />
    </svg>
  );
};

// ============ NEW SPRITES — letters, hazards ============

const LetterBubble = ({ letter }) => (
  <svg viewBox="0 0 44 44" width="44" height="44" style={{ overflow: 'visible' }}>
    <defs>
      <radialGradient id={`lb-${letter}`} cx="35%" cy="30%">
        <stop offset="0%" stopColor="#fff8e0" stopOpacity="0.95"/>
        <stop offset="40%" stopColor="#f5e8c8" stopOpacity="0.5"/>
        <stop offset="100%" stopColor="#d4a84b" stopOpacity="0.85"/>
      </radialGradient>
    </defs>
    <circle cx="22" cy="22" r="20" fill={`url(#lb-${letter})`} stroke="#e8c170" strokeWidth="1.2"/>
    <circle cx="22" cy="22" r="16" fill="none" stroke="#d4a84b" strokeWidth="0.5" strokeDasharray="2 2" opacity="0.5"/>
    <circle cx="14" cy="14" r="3" fill="#fff" opacity="0.6"/>
    <text x="22" y="29" textAnchor="middle"
          fontFamily="Limelight, serif"
          fontSize="22" fontWeight="700"
          fill="#3a1f08"
          stroke="#fff8e0" strokeWidth="0.4">{letter}</text>
  </svg>
);

const TeslaArc = ({ length, dir }) => (
  <svg viewBox={`0 0 ${length} 28`} width={length} height="28" style={{ overflow: 'visible' }}>
    <g transform={dir < 0 ? `scale(-1 1) translate(${-length} 0)` : ''}>
      {[0, 4, 8, 12].map(off => (
        <polyline key={off}
          points={Array.from({ length: 12 }).map((_, i) => {
            const x = (i / 11) * length;
            const y = 14 + Math.sin(i * 1.4 + off) * (5 + (i % 3) * 1.5);
            return `${x},${y}`;
          }).join(' ')}
          stroke="#9bd5ff"
          strokeWidth={off === 0 ? 2.5 : 1}
          fill="none"
          opacity={off === 0 ? 1 : 0.6}/>
      ))}
      <circle cx="0" cy="14" r="6" fill="rgba(255,255,255,0.7)"/>
      <circle cx="0" cy="14" r="3" fill="#fff"/>
    </g>
  </svg>
);

const EmberFlame = () => (
  <svg viewBox="0 0 32 28" width="32" height="28">
    <path d="M 16 28 Q 4 22 8 12 Q 11 16 13 10 Q 16 4 16 0 Q 18 6 20 10 Q 22 14 25 12 Q 28 22 16 28 Z"
          fill="#ff8a3a" opacity="0.9"/>
    <path d="M 16 26 Q 9 22 11 14 Q 13 18 14 13 Q 16 8 16 5 Q 17 10 19 14 Q 21 18 22 14 Q 24 22 16 26 Z"
          fill="#ffd870" opacity="0.85"/>
    <path d="M 16 24 Q 12 21 13 16 Q 15 18 16 12 Q 17 18 19 16 Q 20 21 16 24 Z"
          fill="#fff8e0" opacity="0.7"/>
  </svg>
);

const ChampagneSplash = ({ stage }) => (
  <svg viewBox="0 0 1120 80" width="1120" height="80" preserveAspectRatio="none" style={{ overflow: 'visible' }}>
    <defs>
      <linearGradient id="champ-grad" x1="0" y1="1" x2="0" y2="0">
        <stop offset="0%" stopColor="#f5e8c8" stopOpacity="0.95"/>
        <stop offset="60%" stopColor="#fff8e0" stopOpacity="0.65"/>
        <stop offset="100%" stopColor="#fff" stopOpacity="0"/>
      </linearGradient>
    </defs>
    <rect x="0" y={80 - stage * 80} width="1120" height={stage * 80} fill="url(#champ-grad)"/>
    {Array.from({ length: 30 }).map((_, i) => (
      <circle key={i}
        cx={i * 38 + (stage * 50) % 38}
        cy={80 - stage * 80 + (i % 3) * 8}
        r={1.5 + (i % 3)}
        fill="#fff" opacity={0.5 + (i % 3) * 0.15}/>
    ))}
  </svg>
);

// ============ ROOM RENDER ============

function Room({ idx, room, hero, enemies, bubbles, fruits, letters, hazards, scale, hud, chainCount, HeroComponent }) {
  return (
    <div className="room-stage">
      <div className="room-scaler" style={{
        width: ROOM_W * scale,
        height: ROOM_H * scale,
      }}>
      <div className="room" style={{
        transform: `scale(${scale})`,
      }}>
        {/* BG */}
        <div className="room-bg-sun">
          <DecoSunburstLite />
        </div>
        <div className="room-bg-arch" style={{ left: 80 }} />
        <div className="room-bg-arch" style={{ right: 80 }} />

        {/* Walls + ceiling + floor */}
        <div className="room-ceiling" />
        <div className="room-floor" />
        <div className="room-wall left" />
        <div className="room-wall right" />

        {/* Header */}
        <div className="room-header">
          <div className="room-act">— Act {idx + 1} —</div>
          <div className="room-title">{room.title}</div>
        </div>

        {/* Platforms */}
        {room.platforms.map((p, i) => (
          p.h > 0 ? (
            <div key={i} className="room-plat" style={{
              left: p.x, top: p.y, width: p.w, height: p.h,
            }} />
          ) : null
        ))}

        {/* Fruits */}
        {fruits.map((f, i) => (
          f.collected && !f.anim ? null :
          <div key={'f'+i} className={'fruit ' + (f.anim ? 'collected' : '')}
               style={{ left: f.x, top: f.y }}>
            <FruitSprite kind={f.kind} />
          </div>
        ))}

        {/* Enemies (free-roaming) */}
        {enemies.map((e, i) => (
          (!e.alive || e.trapped) ? null :
          <div key={'e'+i} className="enemy"
               style={{ left: e.x, top: e.y }}>
            <ClockworkEnemy variant={e.variant} />
          </div>
        ))}

        {/* Bubbles */}
        {bubbles.map((b, i) => (
          <div key={'b'+i}
               className={'bubble ' + (b.special ? `bubble-${b.special} ` : '') + (b.popping ? 'popping ' : '') + (b.trapped ? 'trapping' : '')}
               style={{ left: b.x, top: b.y }}>
            {b.trapped && (
              <div className="bubble-trapped">
                <ClockworkEnemy variant={b.variant} />
              </div>
            )}
            {b.special === 'tesla' && !b.popping && (
              <div className="bubble-glyph">⚡</div>
            )}
            {b.special === 'ember' && !b.popping && (
              <div className="bubble-glyph">🔥</div>
            )}
            {b.special === 'champagne' && !b.popping && (
              <div className="bubble-glyph">✦</div>
            )}
          </div>
        ))}

        {/* ENCORE letter bubbles */}
        {letters.map((L, i) => (
          L.collected ? null :
          <div key={'L'+i} className="letter-bubble" style={{ left: L.x, top: L.y }}>
            <LetterBubble letter={L.letter} />
          </div>
        ))}

        {/* Hazards — Tesla arcs, ember flames, champagne flood */}
        {hazards.map((h, i) => {
          if (h.type === 'tesla') {
            return (
              <div key={'h'+i} className="hazard-tesla" style={{
                left: h.dir > 0 ? h.x : h.x - h.length,
                top: h.y,
                opacity: 1 - (h.age / h.life),
              }}>
                <TeslaArc length={h.length} dir={h.dir}/>
              </div>
            );
          }
          if (h.type === 'ember') {
            return (
              <div key={'h'+i} className="hazard-ember" style={{
                left: h.x, top: h.y,
                opacity: 1 - (h.age / h.life),
                transform: `scale(${1 + Math.sin(h.age * 0.4) * 0.1})`,
              }}>
                <EmberFlame/>
              </div>
            );
          }
          if (h.type === 'champagne') {
            return (
              <div key={'h'+i} className="hazard-champagne" style={{
                left: 0,
                bottom: 30,
                opacity: 1 - (h.age / h.life),
              }}>
                <ChampagneSplash stage={Math.min(1, h.age / 22)}/>
              </div>
            );
          }
          if (h.type === 'pop-burst') {
            return (
              <div key={'h'+i} className="hazard-burst" style={{
                left: h.x, top: h.y,
                opacity: 1 - (h.age / h.life),
                transform: `scale(${0.5 + h.age * 0.06})`,
              }}>
                <div className="burst-ring"/>
                <div className="burst-points">+{h.points}</div>
              </div>
            );
          }
          return null;
        })}

        {/* Hero */}
        <div className="hero" style={{
          left: hero.x, top: hero.y,
          opacity: hero.invuln > 0 && Math.floor(hero.invuln / 6) % 2 === 0 ? 0.4 : 1,
        }}>
          <HeroComponent
            running={hero.onGround && Math.abs(hero.vx) > 0.4}
            jumping={!hero.onGround}
            blowing={hero.blowingT > 0}
            frame={hero.runFrame}
            facing={hero.facing}
          />
        </div>

        {/* Hit flash */}
        {hud.flash > 0 && <div className="flash" />}

        {/* Chain combo float */}
        {chainCount > 1 && (
          <div className="chain-display" style={{
            left: hero.x - 20,
            top: hero.y - 50,
          }}>
            <div className="chain-num">×{chainCount}</div>
            <div className="chain-label">CHAIN!</div>
          </div>
        )}

        {/* ENCORE collected display */}
        <div className="encore-track">
          {ENCORE_LETTERS.map((L, i) => (
            <span key={i} className={'encore-letter ' + (hud.encore[i] ? 'lit' : '')}>{L}</span>
          ))}
        </div>
      </div>
      </div>
    </div>
  );
}

const DecoSunburstLite = () => (
  <svg viewBox="0 0 400 400" width="400" height="400">
    <g transform="translate(200 200)" stroke="#d4a84b" fill="none" opacity="0.6">
      {Array.from({ length: 24 }).map((_, i) => (
        <line key={i} x1="0" y1="-180" x2="0" y2="-60" strokeWidth={i % 3 === 0 ? 1.5 : 0.6}
          transform={`rotate(${i * 15})`} />
      ))}
      <circle r="40" strokeWidth="1.5" />
      <circle r="60" strokeWidth="0.6" />
    </g>
  </svg>
);

// ============ APP ============

function AppV2() {
  const [tweaks, setTweak] = useTweaks(/*EDITMODE-BEGIN*/{
    "rain": false,
    "smoke": true,
    "grain": true,
    "scratches": true,
    "vignette": true
  }/*EDITMODE-END*/);

  const [scene, setScene] = useState('title'); // title, select, play, gameover, win, intertitle
  const [intertitle, setIntertitle] = useState(null);
  const [score, setScore] = useState(0);
  const [lives, setLives] = useState(3);
  const [roomIdx, setRoomIdx] = useState(0);
  const [charIdx, setCharIdx] = useState(0);
  const gameKey = useRef(0);

  const beginGame = () => {
    setIntertitle('"A puff of perfume — a prison of light. ' + CHARACTERS[charIdx].name + ' enters the salon."');
    setScene('intertitle');
    setTimeout(() => { setIntertitle(null); setScene('play'); }, 2800);
  };

  useEffect(() => {
    const onKey = (e) => {
      if (e.code === 'Enter') {
        if (scene === 'title') {
          setScene('select');
        } else if (scene === 'select') {
          beginGame();
        } else if (scene === 'gameover' || scene === 'win') {
          gameKey.current++;
          setScore(0); setLives(3); setRoomIdx(0);
          setScene('title');
        }
      } else if (scene === 'select') {
        if (e.code === 'ArrowLeft' || e.code === 'KeyA') {
          setCharIdx(i => (i - 1 + CHARACTERS.length) % CHARACTERS.length);
        } else if (e.code === 'ArrowRight' || e.code === 'KeyD') {
          setCharIdx(i => (i + 1) % CHARACTERS.length);
        }
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [scene, charIdx]);

  return (
    <div className="reel-frame">
      <div className="reel-strip top">
        {Array.from({ length: Math.floor(window.innerWidth / 36) }).map((_, i) => (
          <div key={i} className="sprocket" />
        ))}
      </div>

      <div className="stage-frame">
        <div className="deco-corner tl"><DecoCorner /></div>
        <div className="deco-corner tr"><DecoCorner /></div>
        <div className="deco-corner bl"><DecoCorner /></div>
        <div className="deco-corner br"><DecoCorner /></div>

        {scene === 'play' && (
          <BubbleGame
            key={gameKey.current + '-' + roomIdx}
            roomIdx={roomIdx}
            charIdx={charIdx}
            initialScore={score}
            initialLives={lives}
            onScore={setScore}
            onLives={setLives}
            onGameOver={() => setScene('gameover')}
            onRoomClear={() => {
              if (roomIdx + 1 >= ROOMS.length) {
                setScene('win');
              } else {
                setRoomIdx(i => i + 1);
                setIntertitle(`"Act ${roomIdx + 2} — ${ROOMS[roomIdx + 1].title}"`);
                setScene('intertitle');
                setTimeout(() => { setIntertitle(null); setScene('play'); }, 1800);
              }
            }}
          />
        )}

        {scene !== 'title' && (
          <HUDV2 score={score} lives={lives} roomIdx={roomIdx} />
        )}

        <FXLayerV2 tweaks={tweaks} />

        {scene === 'title' && <TitleCardV2 onStart={() => setScene('select')} />}

        {scene === 'select' && (
          <CharacterSelect
            idx={charIdx}
            onPick={setCharIdx}
            onConfirm={beginGame}
            onBack={() => setScene('title')}
          />
        )}

        {scene === 'intertitle' && intertitle && <IntertitleV2 text={intertitle.replace(/^"|"$/g, '')} />}

        {scene === 'gameover' && <GameOverV2 score={score} onRestart={() => {
          gameKey.current++;
          setScore(0); setLives(3); setRoomIdx(0);
          setScene('title');
        }} />}

        {scene === 'win' && <WinCardV2 score={score} onRestart={() => {
          gameKey.current++;
          setScore(0); setLives(3); setRoomIdx(0);
          setScene('title');
        }} />}
      </div>

      <div className="reel-strip bottom">
        {Array.from({ length: Math.floor(window.innerWidth / 36) }).map((_, i) => (
          <div key={i} className="sprocket" />
        ))}
      </div>
    </div>
  );
}

function HUDV2({ score, lives, roomIdx }) {
  return (
    <div className="hud">
      <div className="hud-card">
        <span className="hud-label">Tokens</span>
        <span className="hud-value">{String(score).padStart(5, '0')}</span>
      </div>
      <div className="hud-card">
        <span className="hud-label">Reel</span>
        <span className="hud-value">Act {roomIdx + 1} / {ROOMS.length}</span>
      </div>
      <div className="hud-card">
        <span className="hud-label">Lives</span>
        <div className="hud-hearts">
          {[0,1,2].map(i => (
            <div key={i} className={'hud-heart ' + (i < lives ? '' : 'empty')} />
          ))}
        </div>
      </div>
    </div>
  );
}

function CharacterSelect({ idx, onPick, onConfirm, onBack }) {
  const c = CHARACTERS[idx];

  const [demoFrame, setDemoFrame] = React.useState(0);
  const [demoBlow, setDemoBlow] = React.useState(false);
  React.useEffect(() => {
    let t = 0;
    const id = setInterval(() => {
      t++;
      setDemoFrame(f => (f + 1) % 4);
      if (t % 6 === 0) setDemoBlow(b => !b);
    }, 200);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="screen-overlay">
      <div className="char-select">
        <div className="char-select-header">
          <div className="char-select-eyebrow">— Cast Call —</div>
          <div className="char-select-title">Who Shall Pursue Them?</div>
          <div className="char-select-divider"><span>✦</span></div>
        </div>

        <div className="char-select-row">
          <button
            className="char-arrow"
            onClick={() => onPick((idx - 1 + CHARACTERS.length) % CHARACTERS.length)}
            aria-label="Previous"
          >◀</button>

          <div className="char-card-stack">
            {CHARACTERS.map((cc, i) => {
              const offset = i - idx;
              const cls = offset === 0 ? 'center' :
                          offset === -1 || offset === CHARACTERS.length - 1 ? 'left' :
                          offset === 1 || offset === -(CHARACTERS.length - 1) ? 'right' : 'far';
              const CharComp = cc.Component;
              return (
                <div key={cc.id} className={`char-card ${cls}`}
                     onClick={() => onPick(i)}>
                  <div className="char-card-frame"/>
                  <div className="char-portrait">
                    <CharComp
                      running={false}
                      jumping={false}
                      blowing={i === idx && demoBlow}
                      frame={demoFrame}
                      facing={1}
                    />
                  </div>
                  <div className="char-card-name">{cc.name}</div>
                  <div className="char-card-title">— {cc.title} —</div>
                </div>
              );
            })}
          </div>

          <button
            className="char-arrow"
            onClick={() => onPick((idx + 1) % CHARACTERS.length)}
            aria-label="Next"
          >▶</button>
        </div>

        <div className="char-bio">
          <div className="char-bio-blurb">"{c.blurb}"</div>
          <div className="char-bio-stats">
            <Stat label="Foot" value={c.stats.speed}/>
            <Stat label="Spring" value={c.stats.jump}/>
            <Stat label="Reach" value={c.stats.range}/>
          </div>
        </div>

        <div className="char-select-actions">
          <div className="char-back" onClick={onBack}>◂ Back</div>
          <div className="char-confirm" onClick={onConfirm}>
            ▸ Press ENTER to begin ◂
          </div>
        </div>
      </div>
    </div>
  );
}

function Stat({ label, value }) {
  return (
    <div className="char-stat">
      <div className="char-stat-label">{label}</div>
      <div className="char-stat-pips">
        {[1,2,3,4,5].map(i => (
          <span key={i} className={'pip ' + (i <= value ? 'lit' : '')}>◆</span>
        ))}
      </div>
    </div>
  );
}

function TitleCardV2({ onStart }) {
  return (
    <div className="screen-overlay">
      <div className="title-card">
        <div className="title-card-frame" />
        <DecoBurst />
        <div className="title-presenting">— A lolomoi Picture —</div>
        <div className="title-main">lolomoi</div>
        <div className="title-sub">A Pursuit in Three Acts</div>
        <div className="title-divider"><span>✦</span></div>
        <div className="title-credits">
          A Bubble-Bound Reverie<br/>
          ◆ Tesla, Ember & Champagne Bubbles ◆<br/>
          ◆ Chains, Combos & the ENCORE Bonus ◆
        </div>
        <div className="title-press" onClick={onStart} style={{ cursor: 'pointer', pointerEvents: 'auto' }}>
          ▸ Press ENTER (or tap) to begin ◂
        </div>
      </div>
    </div>
  );
}

function IntertitleV2({ text }) {
  return (
    <div className="screen-overlay">
      <div className="intertitle">
        <div style={{ marginBottom: 8 }}>"{text}"</div>
      </div>
    </div>
  );
}

function GameOverV2({ score, onRestart }) {
  return (
    <div className="screen-overlay">
      <div className="title-card">
        <div className="title-card-frame" />
        <div className="gameover-stamp">The End</div>
        <div className="title-sub" style={{ marginTop: 30 }}>The Mechanism Wound Down</div>
        <div className="title-divider"><span>✦</span></div>
        <div style={{ fontSize: 20, color: 'var(--cream-bright)', letterSpacing: '0.3em', margin: '20px 0' }}>
          Final score: {String(score).padStart(5, '0')}
        </div>
        <div className="title-press" onClick={onRestart} style={{ cursor: 'pointer', pointerEvents: 'auto' }}>
          ▸ Press ENTER for an encore ◂
        </div>
      </div>
    </div>
  );
}

function WinCardV2({ score, onRestart }) {
  return (
    <div className="screen-overlay">
      <div className="title-card">
        <div className="title-card-frame" />
        <DecoBurst />
        <div className="title-presenting">— Curtain Falls —</div>
        <div className="title-main" style={{ fontSize: 60 }}>Bravissima!</div>
        <div className="title-sub" style={{ marginTop: 20 }}>Every Cog Bubbled · Every Gear Popped</div>
        <div className="title-divider"><span>✦</span></div>
        <div style={{ fontSize: 20, color: 'var(--cream-bright)', letterSpacing: '0.3em', margin: '20px 0' }}>
          Tokens collected: {String(score).padStart(5, '0')}
        </div>
        <div className="title-press" onClick={onRestart} style={{ cursor: 'pointer', pointerEvents: 'auto' }}>
          ▸ Press ENTER for another show ◂
        </div>
      </div>
    </div>
  );
}

function FXLayerV2({ tweaks }) {
  const drops = [];
  if (tweaks.rain) {
    for (let i = 0; i < 60; i++) {
      drops.push(<div key={i} className="rain-drop" style={{
        left: (Math.random() * 100) + '%',
        animationDuration: (0.6 + Math.random() * 0.5) + 's',
        animationDelay: (-Math.random() * 1.2) + 's',
      }} />);
    }
  }
  const puffs = [];
  if (tweaks.smoke) {
    for (let i = 0; i < 4; i++) {
      puffs.push(<div key={i} className="smoke-puff" style={{
        left: (10 + i * 22) + '%',
        bottom: '60px',
        animationDelay: (-i * 1.5) + 's',
      }} />);
    }
  }
  return (
    <div className="fx-layer">
      {drops}
      {puffs}
      {tweaks.scratches && <div className="scratches" />}
      {tweaks.grain && <div className="grain" />}
      {tweaks.vignette && <div className="vignette" />}
    </div>
  );
}

// ============ THE GAME ============

function BubbleGame({ roomIdx, charIdx, initialScore, initialLives, onScore, onLives, onGameOver, onRoomClear }) {
  const [, force] = useState(0);
  const tick = useRef(0);
  const room = ROOMS[roomIdx];

  const hero = useRef({
    x: 80, y: ROOM_H - 30 - 56,
    vx: 0, vy: 0,
    onGround: true, facing: 1,
    runFrame: 0, runTimer: 0,
    invuln: 0, blowingT: 0, cooldown: 0,
  });
  const enemies = useRef(room.enemies.map((e, i) => ({
    ...e, alive: true, trapped: false,
    vx: (i % 2 ? -1 : 1) * PHYS_V2.enemySpeed,
    vy: 0, onGround: false, dir: i % 2 ? -1 : 1,
  })));
  const bubbles = useRef([]);
  const fruits = useRef([]);
  const letters = useRef([]);
  const hazards = useRef([]);
  const keys = useRef({});
  const flashRef = useRef(0);
  const scoreRef = useRef(initialScore || 0);
  const livesRef = useRef(initialLives || 3);
  const chainRef = useRef({ count: 0, timer: 0, lastPoints: 0 });
  const encoreRef = useRef([false, false, false, false, false, false]);
  const bubblesBlownRef = useRef(0);
  const ended = useRef(false);
  const cleared = useRef(false);

  // Get current score/lives from upstream
  useEffect(() => {
    // sync refs from props on remount only
  }, []);

  // Compute scale to fit viewport
  const [scale, setScale] = useState(1);
  useEffect(() => {
    const update = () => {
      const w = window.innerWidth - 80;
      const h = window.innerHeight - 100;
      setScale(Math.min(w / ROOM_W, h / ROOM_H, 1));
    };
    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  useEffect(() => {
    const down = (e) => {
      keys.current[e.code] = true;
      if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space','KeyA','KeyD','KeyW','KeyS','KeyJ','KeyZ'].includes(e.code)) e.preventDefault();
    };
    const up = (e) => { keys.current[e.code] = false; };
    window.addEventListener('keydown', down);
    window.addEventListener('keyup', up);
    return () => {
      window.removeEventListener('keydown', down);
      window.removeEventListener('keyup', up);
    };
  }, []);

  useEffect(() => {
    let raf;
    const loop = () => {
      tick.current++;
      step();
      force(tick.current);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [roomIdx]);

  // Helper: collide rect with platforms. Updates entity.x, y, vx, vy, onGround.
  function platformCollide(ent, w, h) {
    // Walls
    if (ent.x < 24) { ent.x = 24; ent.vx = 0; }
    if (ent.x + w > ROOM_W - 24) { ent.x = ROOM_W - 24 - w; ent.vx = 0; }
    // Floor
    if (ent.y + h > ROOM_H - 30) {
      ent.y = ROOM_H - 30 - h;
      ent.vy = 0;
      ent.onGround = true;
    }
    // Ceiling
    if (ent.y < 30) {
      ent.y = 30;
      if (ent.vy < 0) ent.vy = 0;
    }
    // Platforms — top-only
    for (const p of room.platforms) {
      if (p.h <= 0) continue;
      const eb = ent.y + h, et = ent.y, el = ent.x + 4, er = ent.x + w - 4;
      if (er > p.x && el < p.x + p.w) {
        if (ent.vy >= 0 && eb >= p.y && eb - ent.vy <= p.y + 2) {
          ent.y = p.y - h;
          ent.vy = 0;
          ent.onGround = true;
        }
      }
    }
  }

  function chainBump(basePoints) {
    chainRef.current.count++;
    chainRef.current.timer = 60; // 1s window
    const mult = Math.min(chainRef.current.count, 8);
    const points = basePoints * mult;
    scoreRef.current += points;
    onScore(scoreRef.current);
    chainRef.current.lastPoints = points;
    return points;
  }

  function spawnBurst(x, y, points) {
    hazards.current.push({
      type: 'pop-burst', x, y, age: 0, life: 30, points
    });
  }

  function killEnemy(e, fromX, fromY, basePoints = 200) {
    e.alive = false;
    const points = chainBump(basePoints);
    spawnBurst(fromX, fromY, points);
    const tier = Math.min(2, Math.floor(chainRef.current.count / 3));
    const f = FRUITS[tier];
    fruits.current.push({
      x: fromX, y: fromY, kind: f.name, value: f.value,
      vy: 0, collected: false, anim: false,
    });
    // Maybe spawn an ENCORE letter — once per N kills
    const slotsRemaining = encoreRef.current.findIndex(v => !v);
    if (slotsRemaining !== -1 && Math.random() < 0.35) {
      letters.current.push({
        letter: ENCORE_LETTERS[slotsRemaining],
        slot: slotsRemaining,
        x: fromX,
        y: fromY,
        vy: -3,
        floatPhase: Math.random() * Math.PI * 2,
        collected: false,
      });
    }
  }

  function popSpecialBubble(b) {
    if (b.special === 'tesla') {
      // Lightning arcs in the direction the bubble was last moving
      const dir = (b.facing || (b.vx >= 0 ? 1 : -1));
      const length = 360;
      hazards.current.push({
        type: 'tesla',
        x: b.x + 22, y: b.y + 8,
        dir, length,
        age: 0, life: 18,
      });
      // Kill any enemies in the strip
      const x0 = dir > 0 ? b.x + 22 : b.x + 22 - length;
      const x1 = dir > 0 ? b.x + 22 + length : b.x + 22;
      for (const e of enemies.current) {
        if (!e.alive || e.trapped) continue;
        const ex = e.x + 20, ey = e.y + 20;
        if (ex >= x0 && ex <= x1 && Math.abs(ey - (b.y + 22)) < 36) {
          killEnemy(e, e.x + 8, e.y + 8, 500);
        }
      }
    } else if (b.special === 'ember') {
      // Fire pools on the floor below the bubble
      const fy = ROOM_H - 30 - 28;
      hazards.current.push({
        type: 'ember', x: b.x + 6, y: fy, age: 0, life: 90,
      });
      hazards.current.push({
        type: 'ember', x: b.x - 24, y: fy, age: 0, life: 90,
      });
      hazards.current.push({
        type: 'ember', x: b.x + 36, y: fy, age: 0, life: 90,
      });
    } else if (b.special === 'champagne') {
      // Flood — kill all on-screen enemies, replace with fruits
      hazards.current.push({
        type: 'champagne', x: 0, y: 0, age: 0, life: 60,
      });
      for (const e of enemies.current) {
        if (e.alive && !e.trapped) {
          killEnemy(e, e.x + 8, e.y + 8, 700);
        }
      }
    }
  }

  // Apply tesla-arc damage (called per frame as long as hazard is alive — but we already
  // kill on spawn in popSpecialBubble; this hook is just for embers and champagne damage)
  function tickHazardDamage() {
    for (const hz of hazards.current) {
      if (hz.type === 'ember') {
        for (const e of enemies.current) {
          if (!e.alive || e.trapped) continue;
          if (Math.abs((e.x + 20) - (hz.x + 16)) < 24 && Math.abs((e.y + 20) - (hz.y + 14)) < 24) {
            killEnemy(e, e.x + 8, e.y + 8, 300);
          }
        }
      }
    }
  }

  function step() {
    if (ended.current) return;
    const h = hero.current;
    const k = keys.current;
    const left = k.ArrowLeft || k.KeyA;
    const right = k.ArrowRight || k.KeyD;
    const jump = k.ArrowUp || k.KeyW;
    const blow = k.Space || k.KeyJ || k.KeyZ;

    // Move
    if (left) { h.vx -= PHYS_V2.moveAcc; h.facing = -1; }
    if (right) { h.vx += PHYS_V2.moveAcc; h.facing = 1; }
    if (!left && !right) {
      h.vx *= h.onGround ? PHYS_V2.friction : PHYS_V2.airFriction;
      if (Math.abs(h.vx) < 0.05) h.vx = 0;
    }
    h.vx = Math.max(-PHYS_V2.maxRun, Math.min(PHYS_V2.maxRun, h.vx));

    // Jump
    if (jump && h.onGround) {
      h.vy = PHYS_V2.jumpVel;
      h.onGround = false;
    }
    if (!jump && h.vy < -6) h.vy *= 0.85;

    // Blow bubble
    if (h.cooldown > 0) h.cooldown--;
    if (blow && h.cooldown <= 0) {
      h.cooldown = PHYS_V2.cooldown;
      h.blowingT = 14;
      bubblesBlownRef.current++;

      // Special bubble cadence — every 8th, 14th, 22nd bubble
      let special = null;
      const n = bubblesBlownRef.current;
      if (n % 22 === 0) special = 'champagne';
      else if (n % 14 === 0) special = 'ember';
      else if (n % 8 === 0) special = 'tesla';

      bubbles.current.push({
        x: h.x + (h.facing > 0 ? 24 : -32),
        y: h.y + 6,
        vx: h.facing * PHYS_V2.bubbleSpeed,
        vy: -1,
        life: PHYS_V2.bubbleLife,
        trapped: false,
        popping: false,
        variant: 0,
        special: special,
        facing: h.facing,
        age: 0,
      });
    }
    if (h.blowingT > 0) h.blowingT--;

    // Gravity
    h.vy += PHYS_V2.gravity;
    if (h.vy > 14) h.vy = 14;

    h.x += h.vx;
    h.y += h.vy;
    h.onGround = false;
    platformCollide(h, 36, 56);

    // Bubble platforming — stand on top of bubbles
    for (const b of bubbles.current) {
      if (b.popping) continue;
      const bx = b.x + 22, by = b.y; // top of bubble
      const heroFeet = h.y + 56;
      const heroLeft = h.x + 6, heroRight = h.x + 30;
      // Top-down landing
      if (heroRight > b.x + 6 && heroLeft < b.x + 38) {
        if (h.vy >= 0 && heroFeet >= by + 4 && heroFeet - h.vy <= by + 8) {
          h.y = by + 4 - 56;
          h.vy = 0;
          h.onGround = true;
          // Pop bubble if jump pressed
          if (jump && b.trapped !== false) {
            b.popping = true;
            h.vy = -11; // bigger boost
            if (b.special) {
              popSpecialBubble(b);
            } else if (b.trapped && b.enemyRef) {
              killEnemy(b.enemyRef, b.x + 8, b.y + 8, 200);
            }
            flashRef.current = 4;
            setTimeout(() => {
              bubbles.current = bubbles.current.filter(x => x !== b);
              force(t => t + 1);
            }, 320);
          } else {
            // Standing pushes bubble down slightly
            b.y += 0.5;
          }
        }
      }
    }

    if (h.onGround && Math.abs(h.vx) > 0.4) {
      h.runTimer += Math.abs(h.vx);
      if (h.runTimer > 6) { h.runTimer = 0; h.runFrame = (h.runFrame + 1) % 4; }
    } else {
      h.runFrame = 0;
    }

    if (h.invuln > 0) h.invuln--;
    if (flashRef.current > 0) flashRef.current--;

    // Chain timer
    if (chainRef.current.timer > 0) {
      chainRef.current.timer--;
      if (chainRef.current.timer === 0) chainRef.current.count = 0;
    }

    // Hazards — age, damage, cleanup
    for (const hz of hazards.current) hz.age++;
    tickHazardDamage();
    hazards.current = hazards.current.filter(hz => hz.age < hz.life);

    // Enemies movement
    for (const e of enemies.current) {
      if (!e.alive || e.trapped) continue;
      e.vy += PHYS_V2.gravity;
      if (e.vy > 12) e.vy = 12;
      e.x += e.vx;
      e.onGround = false;
      // Wall bounce (use platform collide for floor)
      if (e.x < 24) { e.x = 24; e.vx = Math.abs(e.vx); e.dir = 1; }
      if (e.x + 40 > ROOM_W - 24) { e.x = ROOM_W - 24 - 40; e.vx = -Math.abs(e.vx); e.dir = -1; }

      e.y += e.vy;
      // Floor
      if (e.y + 40 > ROOM_H - 30) { e.y = ROOM_H - 30 - 40; e.vy = 0; e.onGround = true; }
      // Platforms
      for (const p of room.platforms) {
        if (p.h <= 0) continue;
        const eb = e.y + 40, el = e.x + 4, er = e.x + 36;
        if (er > p.x && el < p.x + p.w) {
          if (e.vy >= 0 && eb >= p.y && eb - e.vy <= p.y + 2) {
            e.y = p.y - 40;
            e.vy = 0;
            e.onGround = true;
            // Reverse if would walk off edge
            if (e.x + 4 < p.x || e.x + 36 > p.x + p.w) {
              // do nothing; we'll detect edges separately
            }
          }
        }
      }
      // Edge detection — turn around if no ground ahead
      if (e.onGround) {
        const probeX = e.dir > 0 ? e.x + 36 + 4 : e.x - 4;
        const probeY = e.y + 42;
        let ground = probeY >= ROOM_H - 30;
        for (const p of room.platforms) {
          if (p.h <= 0) continue;
          if (probeX > p.x && probeX < p.x + p.w && probeY >= p.y && probeY <= p.y + p.h + 2) ground = true;
        }
        if (!ground) { e.dir = -e.dir; e.vx = e.dir * PHYS_V2.enemySpeed; }
      }

      // Hero collision
      if (h.invuln <= 0) {
        if (e.x + 40 > h.x + 6 && e.x < h.x + 30 && e.y + 40 > h.y + 4 && e.y < h.y + 56) {
          h.invuln = 100;
          h.vx = -h.facing * 6;
          h.vy = -8;
          livesRef.current -= 1;
          onLives(livesRef.current);
          if (livesRef.current <= 0) {
            ended.current = true;
            setTimeout(() => onGameOver(), 700);
          }
          flashRef.current = 8;
        }
      }
    }

    // Bubbles
    for (const b of bubbles.current) {
      if (b.popping) continue;
      b.age++;
      if (!b.trapped) {
        // shoot phase
        b.x += b.vx;
        b.y += b.vy;
        b.vx *= 0.96;
        b.vy *= 0.92;
        // Wall bounce
        if (b.x < 26) { b.x = 26; b.vx = Math.abs(b.vx) * 0.5; }
        if (b.x > ROOM_W - 70) { b.x = ROOM_W - 70; b.vx = -Math.abs(b.vx) * 0.5; }
        // Settle into float
        if (Math.abs(b.vx) < 0.5 && b.age > 30) {
          b.vx = 0;
          b.vy = PHYS_V2.bubbleFloat;
        }
        // Trap enemy on contact
        for (const e of enemies.current) {
          if (!e.alive || e.trapped) continue;
          const bx = b.x + 22, by = b.y + 22;
          const ex = e.x + 20, ey = e.y + 20;
          if ((bx-ex)*(bx-ex) + (by-ey)*(by-ey) < 28*28) {
            e.trapped = true;
            b.trapped = true;
            b.variant = e.variant;
            b.enemyRef = e;
            b.vx = 0;
            b.vy = PHYS_V2.bubbleFloat;
            b.age = 0;
            scoreRef.current += 50;
            onScore(scoreRef.current);
            break;
          }
        }
      } else {
        // float upward
        b.y += PHYS_V2.bubbleFloat;
        // sway
        b.x += Math.sin(b.age * 0.06) * 0.3;
      }

      // Ceiling stop
      if (b.y < 36) {
        b.y = 36;
        b.vy = 0;
      }

      // Lifetime expire — release enemy if unpoped
      if (b.age > PHYS_V2.bubbleLife) {
        b.popping = true;
        if (b.trapped && b.enemyRef) {
          b.enemyRef.trapped = false;
          b.enemyRef.x = b.x;
          b.enemyRef.y = b.y;
          b.enemyRef.vy = 0;
        }
        setTimeout(() => {
          bubbles.current = bubbles.current.filter(x => x !== b);
          force(t => t + 1);
        }, 320);
      }

      // Side/under contact pops trapped bubbles only (top is for standing)
      if (!b.popping && (b.trapped || b.special)) {
        const bx = b.x + 22, by = b.y + 22;
        const hx = h.x + 18, hy = h.y + 28;
        const dx2 = bx-hx, dy2 = by-hy;
        if (dx2*dx2 + dy2*dy2 < 26*26 && hy > by - 4) {
          b.popping = true;
          h.vy = -8;
          if (b.special) {
            popSpecialBubble(b);
          } else if (b.enemyRef) {
            killEnemy(b.enemyRef, b.x + 8, b.y + 8, 200);
          }
          flashRef.current = 4;
          setTimeout(() => {
            bubbles.current = bubbles.current.filter(x => x !== b);
            force(t => t + 1);
          }, 320);
        }
      }
    }

    // Fruits — fall to ground, then can be collected
    for (const f of fruits.current) {
      if (f.collected) continue;
      f.vy = (f.vy || 0) + PHYS_V2.gravity;
      f.y += f.vy;
      // Floor
      if (f.y + 26 > ROOM_H - 30) { f.y = ROOM_H - 30 - 26; f.vy = 0; }
      // Platforms
      for (const p of room.platforms) {
        if (p.h <= 0) continue;
        if (f.x + 26 > p.x && f.x < p.x + p.w && f.y + 26 >= p.y && f.y + 26 - f.vy <= p.y + 2) {
          f.y = p.y - 26;
          f.vy = 0;
        }
      }
      // Hero pickup
      const dx = (h.x + 18) - (f.x + 13);
      const dy = (h.y + 28) - (f.y + 13);
      if (dx*dx + dy*dy < 26*26) {
        f.collected = true;
        f.anim = true;
        scoreRef.current += f.value;
        onScore(scoreRef.current);
        setTimeout(() => { f.anim = false; force(t => t + 1); }, 500);
      }
    }

    // ENCORE letters — float around, expire, get picked up
    for (const L of letters.current) {
      if (L.collected) continue;
      L.floatPhase += 0.05;
      L.vy += PHYS_V2.gravity * 0.4;
      if (L.vy > 4) L.vy = 4;
      L.y += L.vy * 0.5;
      L.x += Math.sin(L.floatPhase) * 0.6;
      // bounce off floor
      if (L.y + 44 > ROOM_H - 30) { L.y = ROOM_H - 30 - 44; L.vy = -2.5; }
      // bounce off walls
      if (L.x < 24) L.x = 24;
      if (L.x + 44 > ROOM_W - 24) L.x = ROOM_W - 24 - 44;
      // pickup
      const dx = (h.x + 18) - (L.x + 22);
      const dy = (h.y + 28) - (L.y + 22);
      if (dx*dx + dy*dy < 30*30) {
        L.collected = true;
        encoreRef.current[L.slot] = true;
        scoreRef.current += 500;
        onScore(scoreRef.current);
        spawnBurst(L.x + 8, L.y + 8, 500);
        // ENCORE complete — extra life
        if (encoreRef.current.every(v => v)) {
          encoreRef.current = [false, false, false, false, false, false];
          livesRef.current = Math.min(5, livesRef.current + 1);
          onLives(livesRef.current);
          scoreRef.current += 5000;
          onScore(scoreRef.current);
          spawnBurst(h.x + 18, h.y - 10, 5000);
        }
      }
    }
    letters.current = letters.current.filter(L => !L.collected || L.justCollected);

    // Room clear?
    const remaining = enemies.current.filter(e => e.alive).length;
    if (remaining === 0 && !cleared.current) {
      cleared.current = true;
      ended.current = true;
      setTimeout(() => onRoomClear(), 1000);
    }
  }

  return (
    <>
      <Room
        idx={roomIdx}
        room={room}
        HeroComponent={CHARACTERS[charIdx || 0].Component}
        hero={hero.current}
        enemies={enemies.current}
        bubbles={bubbles.current}
        fruits={fruits.current}
        letters={letters.current}
        hazards={hazards.current}
        scale={scale}
        chainCount={chainRef.current.count}
        hud={{ flash: flashRef.current, encore: encoreRef.current }}
      />
      {cleared.current && (
        <div className="room-clear-banner">
          <div className="room-clear-card">
            <div className="label">Curtain</div>
            <div className="num">Act {roomIdx + 1} Cleared</div>
          </div>
        </div>
      )}
      <TouchControlsV2 keys={keys} />
    </>
  );
}

function TouchControlsV2({ keys }) {
  const press = (code, on) => (e) => {
    e.preventDefault();
    keys.current[code] = on;
    e.currentTarget.classList.toggle('active', on);
  };
  const handlers = (code) => ({
    onTouchStart: press(code, true),
    onTouchEnd: press(code, false),
    onTouchCancel: press(code, false),
    onMouseDown: press(code, true),
    onMouseUp: press(code, false),
    onMouseLeave: press(code, false),
  });
  return (
    <div className="touch-controls">
      <div className="touch-pad">
        <div className="touch-btn" {...handlers('ArrowLeft')}>◀</div>
        <div className="touch-btn" {...handlers('ArrowRight')}>▶</div>
      </div>
      <div className="touch-pad">
        <div className="touch-btn" {...handlers('Space')} style={{ width: 76, height: 76, fontSize: 14 }}>BLOW</div>
        <div className="touch-btn jump" {...handlers('ArrowUp')}>JUMP</div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<AppV2 />);
