import * as bus from '../../../core/bus.js';
import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import { t } from '../../../core/i18n.js';
import { createCanvas, clear } from '../../../core/canvas.js';
import * as rules from '../logic/rules.js';
import * as juice from '../../../core/juice.js';
import { getPiecePosition, getHomeBasePosition, SAFE_SQUARES, HOME_COLUMNS, SHARED_PATH } from '../logic/board-map.js';
import * as mp from '../../../core/multiplayer.js';
import { emoji, getAsset, getColor } from '../../../core/theme.js';
// matchLive import removed — Ludo uses its own consolidated poller to avoid dual-poll conflicts
import * as net from '../../../core/net.js';
import * as modal from '../../../core/modal.js';

let game, validMoves, ctx, canvas, boardSize, cellSize;
let diceAnimating = false;

// 60fps animation system
let boardCanvas = null; // offscreen cached static board
let boardDirty = true;  // flag to redraw static board
let animFrame = null;   // rAF handle
let pieceAnims = new Map(); // pieceId -> {fromX,fromY,toX,toY,t,duration}

// Order: Red(BL), Green(TL), Yellow(TR), Blue(BR) — standard Ludo layout
const DEFAULT_COLORS = ['#E53935', '#43A047', '#FDD835', '#1E88E5'];
const DEFAULT_COLORS_LIGHT = ['#EF9A9A', '#A5D6A7', '#FFF59D', '#90CAF9'];
let COLORS = DEFAULT_COLORS;
let COLORS_LIGHT = DEFAULT_COLORS_LIGHT;
const PAWN_SLOTS = ['ludo_pawn_red', 'ludo_pawn_green', 'ludo_pawn_yellow', 'ludo_pawn_blue'];
const pawnImages = [null, null, null, null];
let PLAYER_NAMES = [t('common.you'), 'Bot 1', 'Bot 2', 'Bot 3'];

function lightenColor(hex, amount) {
  const num = parseInt(hex.replace('#', ''), 16);
  const r = Math.min(255, ((num >> 16) & 0xFF) + Math.round(255 * amount));
  const g = Math.min(255, ((num >> 8) & 0xFF) + Math.round(255 * amount));
  const b = Math.min(255, (num & 0xFF) + Math.round(255 * amount));
  return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
}
let livePoller = null;
let myPlayerIndex = 0;
let matchId = null;
let isHost = false;
let turnTimer = null;
let turnTimerStart = 0;
let livePlayerIds = [];
const TURN_TIMEOUT = 15000;
const TURBO_SPEED = 0.4;

function renderPanel(p) {
  return `
    <div class="pp" id="pp-${p.i}" style="--pc:${p.color};">
      <div class="pp-turn-arrow">▶</div>
      <div class="pp-avatar" style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,var(--bg-hover),#1a1a3a);border:2.5px solid ${p.color};display:flex;align-items:center;justify-content:center;font-size:14px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.3);">
        ${p.avatar}
      </div>
      <div style="display:flex;flex-direction:column;">
        <span class="pp-name" style="font-size:11px;font-weight:700;color:var(--text-primary);">${p.name}</span>
        <span class="pp-status" style="font-size:9px;color:var(--text-muted);">${p.level || ''}</span>
      </div>
      <div class="pp-dice" id="dice-${p.i}"></div>
    </div>
  `;
}

export function mountGame(el, params) {
  const { mode = 'bot', seats, humanCount = 1, difficulty = 'medium' } = params;
  const numPlayers = params.numPlayers || params.playerCount || 4;
  scene.enterGameMode();

  myPlayerIndex = params.playerIndex || 0;
  matchId = params.matchId || null;
  isHost = myPlayerIndex === 0;
  livePlayerIds = params.players || [];

  // Determine active seat indices (which of [0,1,2,3] are playing)
  // 2 players: diagonal (Red BL vs Yellow TR), 3 players: skip Yellow
  const defaultSeats = numPlayers === 2 ? [0, 2] : numPlayers === 3 ? [0, 1, 3] : [0, 1, 2, 3];
  const activeSeats = seats || defaultSeats;

  if (mode === 'live' && params.players) {
    PLAYER_NAMES = params.players.map((p, i) => {
      if (i === myPlayerIndex) return t('common.you');
      if (p.startsWith('bot')) return 'Bot ' + p.split('_')[1];
      return t('common.player') + ' ' + (i + 1);
    });
  } else if (mode === 'local-multi') {
    PLAYER_NAMES = activeSeats.map((seatIdx, i) => {
      if (i === 0) return t('common.you');
      if (i < humanCount) return t('common.player') + ' ' + (i + 1);
      return 'Bot ' + (i - humanCount + 1);
    });
  } else {
    PLAYER_NAMES = [t('common.you'), 'Bot 1', 'Bot 2', 'Bot 3'].slice(0, numPlayers);
  }

  game = rules.createGame(numPlayers, activeSeats);
  game.mode = mode;
  game.startTime = Date.now();
  game.turboMode = false;
  game.turnCount = 0;
  game.activeSeats = activeSeats;
  game.difficulty = difficulty;
  game.humanPlayers = PLAYER_NAMES.map((n, i) => n.startsWith('Bot') ? -1 : i).filter(i => i >= 0);
  validMoves = [];
  diceAnimating = false;
  endingGame = false;
  lastSyncTurnCount = 0;
  if (livePoller) { clearInterval(livePoller); livePoller = null; }

  const player = store.get('player') || {};
  const panels = Array.from({ length: numPlayers }, (_, i) => {
    const isMe = i === myPlayerIndex;
    const isBot = PLAYER_NAMES[i]?.startsWith('Bot');
    const avatar = isMe && player.avatar_url ? `<img src="${player.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : isBot ? emoji('robot', '🤖', 14) : emoji('person', '👤', 14);
    const name = isMe ? (player.display_name || player.username || t('common.you')) : (PLAYER_NAMES[i] || 'Bot');
    const boardSlot = activeSeats[i] ?? i;
    const level = isMe ? `Lv.${player.level || 1}` : (isBot ? '' : '');
    return { i, avatar, name, level, color: COLORS[boardSlot] };
  });

  el.innerHTML = `
    <div style="display:flex;flex-direction:column;height:100%;background:linear-gradient(180deg,#0d0d1a 0%,var(--bg-card) 50%,#0d0d1a 100%);justify-content:flex-end;">
      <div style="display:flex;justify-content:space-between;padding:8px 12px;background:rgba(15,15,30,0.9);border-bottom:1px solid rgba(255,255,255,0.04);direction:ltr;">
        ${numPlayers === 2 ? '<div></div>' : (panels.length > 1 ? renderPanel(panels[1]) : '<div></div>')}
        ${numPlayers === 2 ? renderPanel(panels[1]) : (panels.length > 2 ? renderPanel(panels[2]) : '<div></div>')}
      </div>
      <div id="ludo-wrap" style="flex:1;display:flex;align-items:center;justify-content:center;padding:6px;min-height:0;"></div>
      <div style="display:flex;justify-content:space-between;padding:8px 12px;background:rgba(15,15,30,0.9);border-top:1px solid rgba(255,255,255,0.04);direction:ltr;">
        ${renderPanel(panels[0])}
        ${panels.length > 3 ? renderPanel(panels[3]) : '<div></div>'}
      </div>
      <div id="turn-timer-bar" style="height:4px;background:rgba(255,255,255,0.05);position:relative;overflow:hidden;"><div id="turn-timer-fill" style="position:absolute;inset:0;background:linear-gradient(90deg,var(--green-light),var(--gold),var(--error));transform:scaleX(1);transform-origin:left;transition:none;"></div></div>
      <div id="dice-area" style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:linear-gradient(180deg,#12122a,var(--bg-deep));border-top:1px solid rgba(228,172,56,0.15);justify-content:center;padding-bottom:max(14px, env(safe-area-inset-bottom, 0px));position:relative;overflow:hidden;transition:background 0.4s ease;">
        <div style="position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0%,rgba(228,172,56,0.06) 0%,transparent 70%);pointer-events:none;"></div>
        <button class="btn btn-secondary" id="exit-btn" style="min-height:44px;min-width:44px;padding:0;font-size:13px;color:var(--error);border-radius:50%;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);">✕</button>
        <button class="btn btn-secondary" id="emote-btn" style="min-height:44px;min-width:44px;padding:0;font-size:18px;border-radius:50%;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);">${emoji('smiley', '😄', 18)}</button>
        <div id="dice-box" style="width:56px;height:56px;background:linear-gradient(145deg,#ffffff,#f0ede8);border-radius:12px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:7px;box-shadow:0 4px 12px rgba(0,0,0,0.4),inset 0 2px 0 rgba(255,255,255,0.9),0 0 0 2px rgba(228,172,56,0.15);transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1),opacity 0.3s ease,filter 0.3s ease;">
        </div>
        <button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 32px;min-height:52px;border-radius:14px;background:linear-gradient(135deg,var(--gold),var(--warning));box-shadow:0 4px 14px rgba(228,172,56,0.3);font-weight:800;transition:background 0.3s ease,color 0.3s ease,opacity 0.3s ease;" disabled>${t('game.roll_dice')}</button>
        <div id="turn-status" style="position:absolute;top:0;left:50%;transform:translateX(-50%);font-size:11px;font-weight:700;padding:2px 12px;border-radius:0 0 8px 8px;display:none;"></div>
      </div>
    </div>
    <style>
      .pp{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:10px;border:2px solid transparent;transition:all 0.3s cubic-bezier(0.34,1.56,0.64,1);position:relative;}
      .pp.active{border-color:var(--pc);background:rgba(255,255,255,0.06);animation:panelPulse 2s ease-in-out infinite;box-shadow:0 0 12px color-mix(in srgb, var(--pc) 20%, transparent);}
      .pp-turn-arrow{position:absolute;left:-2px;top:50%;transform:translateY(-50%) scale(0);font-size:10px;color:var(--pc);transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1);filter:drop-shadow(0 0 4px var(--pc));}
      .pp.active .pp-turn-arrow{transform:translateY(-50%) scale(1);animation:arrowBounce 1s ease-in-out infinite;}
      .pp-name{transition:color 0.3s,text-shadow 0.3s;}
      .pp.active .pp-name{color:#fff !important;text-shadow:0 0 8px color-mix(in srgb, var(--pc) 50%, transparent);}
      .pp-dice{width:28px;height:28px;background:linear-gradient(145deg,#ffffff,#f0ede8);border-radius:6px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:3px;box-shadow:0 2px 6px rgba(0,0,0,0.3);transition:all 0.2s cubic-bezier(0.34,1.56,0.64,1);opacity:0;transform:scale(0);margin-left:auto;}
      .pp-dice.visible{opacity:1;transform:scale(1);}
      .pp-dice.rolling{animation:diceShake 0.08s infinite alternate;opacity:1;transform:scale(1);}
      .pp-dice.landed{opacity:1;transform:scale(1.15);box-shadow:0 0 12px rgba(228,172,56,0.5);}
      .pp.active .pp-avatar{animation:avatarBounce 2s ease-in-out infinite;box-shadow:0 0 12px color-mix(in srgb, var(--pc) 40%, transparent);}
      @keyframes panelPulse{0%,100%{box-shadow:0 0 8px 0 transparent;}50%{box-shadow:0 0 16px 3px color-mix(in srgb, var(--pc) 25%, transparent);}}
      @keyframes avatarBounce{0%,100%{transform:scale(1);}50%{transform:scale(1.1);}}
      @keyframes arrowBounce{0%,100%{transform:translateY(-50%) scale(1) translateX(0);}50%{transform:translateY(-50%) scale(1) translateX(3px);}}
      @keyframes diceShake{from{transform:rotate(-15deg) scale(0.85);}to{transform:rotate(15deg) scale(0.85);}}
      @keyframes dicePopIn{0%{transform:scale(0) rotate(-30deg);opacity:0;}60%{transform:scale(1.3) rotate(5deg);opacity:1;}100%{transform:scale(1) rotate(0deg);opacity:1;}}
      @keyframes diceSix{0%,100%{box-shadow:0 0 8px rgba(228,172,56,0.4);}50%{box-shadow:0 0 20px rgba(228,172,56,0.9);}}
      @keyframes rollBtnPulse{0%,100%{box-shadow:0 4px 14px rgba(228,172,56,0.3);}50%{box-shadow:0 4px 24px rgba(228,172,56,0.6);}}
      #roll-btn:not([disabled]){animation:rollBtnPulse 2s ease-in-out infinite;}
      @keyframes emojiBubble{0%{opacity:0;transform:translate(-50%,-100%) scale(0);}15%{opacity:1;transform:translate(-50%,-100%) scale(1.3);}70%{opacity:1;transform:translate(-50%,-130%) scale(1);}100%{opacity:0;transform:translate(-50%,-160%) scale(0.7);}}
      @keyframes phraseBubble{0%{opacity:0;transform:translate(-50%,-80%) scale(0.7);}15%{opacity:1;transform:translate(-50%,-100%) scale(1);}75%{opacity:1;transform:translate(-50%,-100%) scale(1);}100%{opacity:0;transform:translate(-50%,-120%) scale(0.8);}}
      @keyframes fadeIn{from{opacity:0;transform:translateX(-50%) translateY(8px);}to{opacity:1;transform:translateX(-50%) translateY(0);}}
      .lep-emoji:active,.lep-phrase:active{transform:scale(0.9);background:rgba(228,172,56,0.15)!important;}
    </style>
  `;

  const wrap = el.querySelector('#ludo-wrap');
  const availableH = wrap.clientHeight || 300;
  const availableW = wrap.clientWidth || 360;
  boardSize = Math.min(availableW - 8, availableH - 8, 420);
  cellSize = boardSize / 15;
  const { canvas: c, ctx: cx } = createCanvas(wrap, boardSize, boardSize);
  c.style.cssText = 'border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.4);max-width:100%;max-height:100%;';
  canvas = c; ctx = cx;

  // Create offscreen canvas for static board cache
  boardCanvas = document.createElement('canvas');
  boardCanvas.width = boardSize;
  boardCanvas.height = boardSize;
  boardDirty = true;
  pieceAnims = new Map();

  // Load themed pawn colors
  COLORS = [
    getColor('ludo_red', DEFAULT_COLORS[0]),
    getColor('ludo_green', DEFAULT_COLORS[1]),
    getColor('ludo_yellow', DEFAULT_COLORS[2]),
    getColor('ludo_blue', DEFAULT_COLORS[3]),
  ];
  COLORS_LIGHT = COLORS.map(c => lightenColor(c, 0.4));

  // Preload custom pawn sprites (non-blocking)
  PAWN_SLOTS.forEach((slot, i) => {
    const url = getAsset(slot);
    if (url) {
      const img = new Image();
      img.src = url;
      img.onload = () => { pawnImages[i] = img; };
    } else {
      pawnImages[i] = null;
    }
  });

  renderDiceFace(el.querySelector('#dice-box'), 1);
  for (let i = 0; i < numPlayers; i++) {
    const md = el.querySelector(`#dice-${i}`);
    if (md) { renderMiniDice(md, 1); md.className = 'pp-dice'; }
  }
  drawBoard();
  startRenderLoop();
  startHeartbeat();
  updatePanels(el);
  el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el));
  el.querySelector('#exit-btn').addEventListener('click', () => handleExit(el));
  el.querySelector('#emote-btn').addEventListener('click', () => showEmotePanel(el));

  // Emotes + multiplayer (panel created on demand via emote-btn)

  // Live mode setup — consolidated polling (no matchLive.start to avoid dual-poller conflict)
  if (mode === 'live' && matchId) {
    // Only use matchLive for session persistence (tab recovery), not for polling
    localStorage.setItem('el3ab_active_match', JSON.stringify({ matchId, gameType: 'ludo', timestamp: Date.now() }));

    mp.onEmoteReceived((emote) => {
      const senderIdx = emote.from === store.get('auth.userId') ? myPlayerIndex : findPlayerByUserId(emote.from);
      showEmoteBubble(el, senderIdx, emote.key);
      audio.play('sfx_emote', 'ui');
    });

    // Fetch opponent profiles and update player panels (name + avatar + tap for profile)
    if (params.players) {
      params.players.forEach((pId, i) => {
        if (i === myPlayerIndex || pId.startsWith('bot')) return;
        mp.fetchOpponentProfile(pId).then(profile => {
          if (profile && !profile.error) {
            const panel = el.querySelector(`#pp-${i}`);
            if (panel) {
              const nameEl = panel.querySelector('.pp-name');
              const displayName = profile.display_name || profile.username || t('common.player');
              if (nameEl) nameEl.textContent = displayName;
              PLAYER_NAMES[i] = displayName;
              const avatarEl = panel.querySelector('.pp-avatar');
              if (avatarEl && profile.avatar_url) {
                avatarEl.innerHTML = `<img src="${profile.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`;
              }
              panel.style.cursor = 'pointer';
              panel.addEventListener('click', () => showOpponentPopup(el, profile));
            }
          }
        });
      });
    }

    // If not my turn at start, begin polling
    if (!isMyTurn()) handleNonPlayerTurn(el);
  }

  bus.emit('game:started', { gameKey: 'ludo', mode });
}

function renderDiceFace(diceBox, value) {
  const diceUrl = getAsset('emoji_dice_' + value);
  if (diceUrl) {
    diceBox.innerHTML = `<img src="${diceUrl}" style="width:100%;height:100%;object-fit:contain;" draggable="false">`;
    diceBox.style.padding = '0';
    diceBox.style.display = 'flex';
    diceBox.style.gridTemplate = 'none';
    return;
  }
  diceBox.style.padding = '6px';
  diceBox.style.display = 'grid';
  diceBox.style.gridTemplate = 'repeat(3,1fr)/repeat(3,1fr)';
  const dots = {
    1: [0,0,0,0,1,0,0,0,0],
    2: [0,0,1,0,0,0,1,0,0],
    3: [0,0,1,0,1,0,1,0,0],
    4: [1,0,1,0,0,0,1,0,1],
    5: [1,0,1,0,1,0,1,0,1],
    6: [1,0,1,1,0,1,1,0,1],
  };
  const pattern = dots[value] || dots[1];
  diceBox.innerHTML = pattern.map(d =>
    `<div style="width:10px;height:10px;border-radius:50%;background:${d ? 'var(--bg-card)' : 'transparent'};margin:auto;transition:transform 0.1s;${d ? 'box-shadow:0 1px 2px rgba(0,0,0,0.3);' : ''}"></div>`
  ).join('');
}

function isMyTurn() {
  if (game.mode === 'local-multi') {
    return game.humanPlayers.includes(game.currentPlayer);
  }
  return game.currentPlayer === myPlayerIndex;
}

function findPlayerByUserId(userId) {
  if (!livePlayerIds || !livePlayerIds.length) return 0;
  const idx = livePlayerIds.indexOf(userId);
  return idx >= 0 ? idx : 0;
}

async function handleRoll(el) {
  const activePlayer = game.currentPlayer;
  if (diceAnimating || game.rolled || game.gameOver || !isMyTurn() || game.players[activePlayer].finished) return;
  clearTurnTimer();
  diceAnimating = true;
  updatePanels(el);

  const dice = await animateDice(el, activePlayer);
  game.diceValue = dice;
  game.rolled = true;
  diceAnimating = false;
  updatePanels(el);

  // Star burst on 6
  if (dice === 6) {
    const rect = el.querySelector('#dice-box').getBoundingClientRect();
    juice.starBurst(rect.left + 31, rect.top + 31, 8);
  }

  validMoves = rules.getValidMoves(game, activePlayer, dice);
  if (validMoves.length === 0) {
    setTimeout(() => {
      game.rolled = false;
      rules.nextTurn(game);
      if (game.mode === 'live') syncLudoState(dice);
      updatePanels(el);
      drawBoard();
      if (!isMyTurn()) handleNonPlayerTurn(el);
    }, 800);
  } else if (validMoves.length === 1) {
    highlightMovablePieces(validMoves);
    setTimeout(() => doMove(el, validMoves[0]), 600);
  } else {
    highlightMovablePieces(validMoves);
    waitForPieceSelection(el, validMoves);
    startTurnTimer(el, () => {
      // Auto-pick best move on timeout
      if (selectionListener) { canvas.removeEventListener('click', selectionListener); selectionListener = null; }
      highlightedPieces = [];
      const best = rules.getBotMove(game, activePlayer, dice);
      if (best) doMove(el, best);
    });
  }
}

function doMove(el, move) {
  animateMove(el, move);
}

function handleNonPlayerTurn(el) {
  if (game.mode === 'live') {
    const currentPlayerStr = PLAYER_NAMES[game.currentPlayer];
    if (currentPlayerStr && currentPlayerStr.startsWith('Bot') && isHost) {
      // Host runs bots locally and syncs result to server
      stopLudoPolling();
      setTimeout(() => botLoop(el), 400);
    } else if (!livePoller) {
      // Non-host (or waiting for human opponent): poll for state updates (avoid double-poll)
      startLudoPolling(el);
    }
  } else {
    // Single player — all non-player turns are bots
    setTimeout(() => botLoop(el), 400);
  }
}

async function botLoop(el) {
  if (game.gameOver || isMyTurn() || game.players[game.currentPlayer].finished) return;

  // === BOT PERSONALITY PROFILES ===
  const personalities = {
    1: { name: 'aggressive', thinkMin: 500, thinkMax: 1000, preferCapture: true },
    2: { name: 'casual', thinkMin: 1500, thinkMax: 3000, suboptimal: true },
    3: { name: 'strategic', thinkMin: 1000, thinkMax: 2000, optimal: true },
  };
  const personality = personalities[game.currentPlayer] || personalities[1];

  // === BOT HUMANIZATION: delays that mimic real player ===
  const turboMul = game.turboMode ? TURBO_SPEED : 1;
  const thinkDelay = (personality.thinkMin + Math.random() * (personality.thinkMax - personality.thinkMin)) * turboMul;

  const botPanel = el.querySelector(`#pp-${game.currentPlayer}`);
  const botStatusEl = botPanel ? botPanel.querySelector('.pp-status') : null;
  const originalStatusText = botStatusEl ? botStatusEl.textContent : '';
  if (botStatusEl) botStatusEl.textContent = t('game.thinking');

  await new Promise(r => setTimeout(r, thinkDelay));
  if (game.gameOver || isMyTurn()) { if (botStatusEl) botStatusEl.textContent = originalStatusText; return; }

  // 2. Roll dice with SAME animation as player
  if (botStatusEl) botStatusEl.textContent = originalStatusText;
  const dice = await animateDice(el, game.currentPlayer);

  // 3. "Deciding" which piece to move — personality-driven delay
  const decideDelay = (personality.thinkMin * 0.5 + Math.random() * (personality.thinkMax - personality.thinkMin) * 0.5) * turboMul;

  // Show thinking indicator again while deciding
  if (botStatusEl) botStatusEl.textContent = t('game.thinking');
  await new Promise(r => setTimeout(r, decideDelay));
  if (botStatusEl) botStatusEl.textContent = originalStatusText;
  if (game.gameOver || isMyTurn()) return;

  // 4. Execute move with step animation
  game.diceValue = dice;
  const move = rules.getBotMove(game, game.currentPlayer, dice);
  if (!move) {
    // No valid moves — just pass turn
    if (game.gameOver) {
      if (game.mode === 'live' && isHost) syncLudoState(dice);
      endGame(el);
      return;
    }
    rules.nextTurn(game);
    updatePanels(el);
    drawBoard();
    if (game.mode === 'live' && isHost) syncLudoState(dice);
    if (!isMyTurn()) handleNonPlayerTurn(el);
    return;
  }

  // Animate bot move step by step
  const pIdx = parseInt(move.pieceId.split('-')[0]);
  const pieceIdx = parseInt(move.pieceId.split('-')[1]);
  const piece = game.players[pIdx].pieces[pieceIdx];
  const fromPos = piece.pos;
  const toPos = move.to;

  if (move.type !== 'enter' && fromPos >= 0 && toPos > fromPos && toPos - fromPos <= 6) {
    const hopDuration = game.turboMode ? 120 : 200;
    const bounceHeight = cellSize * 0.6;
    for (let i = 1; i <= toPos - fromPos; i++) {
      piece.pos = fromPos + i;
      await tween(hopDuration, t => {
        const arc = -4 * bounceHeight * t * (t - 1);
        const squash = t > 0.8 ? (t - 0.8) * 5 * 2 : 0;
        piece._bounceOffset = -arc + squash;
      });
      piece._bounceOffset = 0;
      audio.play('move', 'game');
    }
    piece.pos = fromPos;
  }

  rules.applyMove(game, game.currentPlayer, move);

  if (move.type === 'capture') {
    const capturedHuman = move._capturedPlayers && move._capturedPlayers.some(p =>
      game.mode === 'local-multi' ? game.humanPlayers.includes(p) : p === myPlayerIndex
    );
    if (capturedHuman) {
      audio.play('lose', 'game');
      juice.hapticError();
      juice.shake(el, 8, 400);
      juice.screenFlash('rgba(229,57,53,0.25)', 500);
    } else {
      audio.play('capture', 'game');
      juice.shake(el, 3, 150);
    }
  } else if (move.type === 'finish') {
    audio.play('notification');
    const boardRect = canvas.getBoundingClientRect();
    const finishSlot = game.players[game.currentPlayer]?.boardSlot ?? game.currentPlayer;
    fireworkBurst(boardRect.left + boardRect.width / 2, boardRect.top + boardRect.height / 2, COLORS[finishSlot]);
  }

  if (move.type === 'capture' && Math.random() > 0.5) {
    setTimeout(() => {
      const botEmotes = ['😂', '💪', '🎉', '😎'];
      showEmoteBubble(el, game.currentPlayer, botEmotes[Math.floor(Math.random() * botEmotes.length)]);
    }, 300);
  }

  if (game.gameOver) {
    if (game.mode === 'live' && isHost) syncLudoState(dice);
    endGame(el);
    return;
  }
  rules.nextTurn(game);
  updatePanels(el);
  drawBoard();

  if (game.mode === 'live' && isHost) syncLudoState(dice);

  if (!isMyTurn()) handleNonPlayerTurn(el);
}

// === LIVE SYNC ===
let lastSyncTurnCount = 0;

function syncLudoState(diceRolled = null) {
  if (!matchId) return;
  const turnCount = (game.turnCount || 0) + 1;
  game.turnCount = turnCount;
  const positions = game.players.map(p => ({
    pos: p.pieces.map(pc => pc.pos),
    finished: p.finished
  }));
  const payload = {
    action: 'move',
    match_id: matchId,
    positions,
    current_turn: game.currentPlayer,
    dice_value: game.diceValue,
    game_state: JSON.stringify({ turn_count: turnCount, dice_rolled: diceRolled }),
  };
  if (game.gameOver) {
    payload.status = 'completed';
    payload.winners = game.winners;
  }
  net.post('ludo-match.php', payload).catch(() => {});
}

function startLudoPolling(el) {
  if (livePoller) return;
  livePoller = setInterval(async () => {
    if (game.gameOver) { stopLudoPolling(); return; }
    if (isMyTurn() && !diceAnimating) { stopLudoPolling(); return; }
    try {
      const data = await net.post('ludo-match.php', { action: 'get', match_id: matchId });
      if (!data || data.error) return;

      // Track opponent activity for disconnect detection
      mp.updateConnectionStatus(true);

      // Server detected turn timeout
      if (data._turn_timed_out && data._replace_with_bot && data._timeout_player != null) {
        PLAYER_NAMES[data._timeout_player] = 'Bot ' + data._timeout_player;
      }

      // Parse game_state
      let gs = {};
      try { gs = typeof data.game_state === 'string' ? JSON.parse(data.game_state) : (data.game_state || {}); } catch(e) {}
      const remoteTurnCount = gs.turn_count || 0;

      // Only process if server state advanced
      if (remoteTurnCount <= lastSyncTurnCount) return;
      lastSyncTurnCount = remoteTurnCount;

      // Detect position changes for remote dice display
      const positions = typeof data.positions === 'string' ? JSON.parse(data.positions) : data.positions;
      if (positions) {
        let posChanged = false;
        positions.forEach((p, pIdx) => {
          if (!p.pos) return;
          p.pos.forEach((newPos, pieceIdx) => {
            const piece = game.players[pIdx]?.pieces[pieceIdx];
            if (piece && piece.pos !== newPos) posChanged = true;
          });
          if (p.finished !== undefined) game.players[pIdx].finished = p.finished;
        });

        // Apply positions
        positions.forEach((p, pIdx) => {
          if (p.pos) p.pos.forEach((pos, pieceIdx) => {
            game.players[pIdx].pieces[pieceIdx].pos = pos;
          });
        });

        // Show remote dice result on the mover's mini dice
        if (gs.dice_rolled != null && posChanged) {
          const prevPlayer = game.currentPlayer;
          const moverIdx = data.current_turn !== undefined && data.current_turn !== prevPlayer ? prevPlayer : null;
          if (moverIdx != null) {
            showRemoteDice(el, moverIdx, gs.dice_rolled);
          }
        }
      }

      // Update turn
      game.currentPlayer = data.current_turn ?? game.currentPlayer;
      game.diceValue = data.dice_value;
      game.turnCount = remoteTurnCount;

      // Check if game ended
      if (data.status === 'completed' && !game.gameOver) {
        // Parse server winners into local game.winners
        if (data.winners) {
          try {
            const serverWinners = typeof data.winners === 'string' ? JSON.parse(data.winners) : data.winners;
            if (Array.isArray(serverWinners) && serverWinners.length > 0) {
              game.winners = serverWinners;
            }
          } catch (e) {}
        }
        // Fallback: if still no winners, derive from positions (who has all pieces finished)
        if (!game.winners || game.winners.length === 0) {
          game.winners = game.players
            .map((p, i) => p.finished ? i : -1)
            .filter(i => i >= 0);
        }
        game.gameOver = true;
        stopLudoPolling();
        endGame(el);
        return;
      }

      // CRITICAL: Reset rolled state when turn comes to me
      if (isMyTurn()) {
        game.rolled = false;
        game.diceValue = null;
      }

      updatePanels(el);
      drawBoard();

      // If now my turn, stop polling and notify
      if (isMyTurn()) {
        stopLudoPolling();
        audio.play('sfx_turn_start', 'ui');
        notifyMyTurn(el);
        const mainDice = el.querySelector('#dice-box');
        if (mainDice) renderDiceFace(mainDice, 1);
      } else {
        // If it's a bot's turn and I'm host, run bot
        const currentPlayerStr = PLAYER_NAMES[game.currentPlayer];
        if (currentPlayerStr && currentPlayerStr.startsWith('Bot') && isHost) {
          stopLudoPolling();
          setTimeout(() => botLoop(el), 400);
        }
      }
    } catch (e) {}
  }, 1500);
}

function stopLudoPolling() {
  if (livePoller) { clearInterval(livePoller); livePoller = null; }
}

function showRemoteDice(el, playerIdx, value) {
  const miniDice = el.querySelector(`#dice-${playerIdx}`);
  if (miniDice) {
    renderMiniDice(miniDice, value);
    miniDice.className = 'pp-dice landed';
    miniDice.style.animation = 'dicePopIn 0.35s cubic-bezier(0.34,1.56,0.64,1)';
    if (value === 6) miniDice.style.animation += ', diceSix 1s ease-in-out infinite';
    setTimeout(() => { miniDice.className = 'pp-dice'; miniDice.style.animation = ''; }, 2000);
  }
}

// Heartbeat: ping server every 10s to track connection state
let heartbeatTimer = null;
function startHeartbeat() {
  if (!matchId || game.mode !== 'live') return;
  heartbeatTimer = setInterval(() => {
    if (game.gameOver) { clearInterval(heartbeatTimer); heartbeatTimer = null; return; }
    net.post('ludo-match.php', { action: 'heartbeat', match_id: matchId }).catch(() => {});
  }, 10000);
}

// ===== OPPONENT POPUP =====
function showOpponentPopup(el, profile) {
  const existing = document.getElementById('ludo-opp-popup');
  if (existing) { existing.remove(); return; }

  const popup = document.createElement('div');
  popup.id = 'ludo-opp-popup';
  popup.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--bg-card);border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:20px;z-index:200;box-shadow:0 12px 40px rgba(0,0,0,0.7);text-align:center;min-width:200px;';
  popup.innerHTML = `
    <div style="width:48px;height:48px;border-radius:50%;background:var(--bg-hover);margin:0 auto 8px;display:flex;align-items:center;justify-content:center;font-size:22px;">
      ${profile.avatar_url ? `<img src="${profile.avatar_url}" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">` : emoji('person', '👤', 14)}
    </div>
    <div style="font-size:15px;font-weight:700;color:var(--text-primary);margin-bottom:4px;">${profile.display_name || profile.username}</div>
    <div style="font-size:11px;color:var(--text-muted);margin-bottom:14px;">Level ${profile.level || 1}</div>
    <div style="display:flex;gap:8px;justify-content:center;">
      <button id="opp-add-friend" style="padding:8px 16px;background:var(--chess-primary);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;">${emoji('plus', '➕', 12)} ${t('mp.add_friend')}</button>
      <button id="opp-close" style="padding:8px 16px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:var(--text-secondary);font-size:12px;cursor:pointer;">${t('common.close')}</button>
    </div>
  `;
  document.body.appendChild(popup);

  popup.querySelector('#opp-close').addEventListener('click', () => popup.remove());
  popup.querySelector('#opp-add-friend').addEventListener('click', async () => {
    await mp.addFriendFromGame(profile.id);
    popup.querySelector('#opp-add-friend').textContent = t('common.done');
    popup.querySelector('#opp-add-friend').style.background = 'var(--success)';
    juice.hapticLight();
    setTimeout(() => popup.remove(), 1000);
  });

  // Close on outside click
  setTimeout(() => {
    document.addEventListener('click', function close(e) {
      if (!popup.contains(e.target)) { popup.remove(); document.removeEventListener('click', close); }
    });
  }, 200);
}

// ===== PIECE SELECTION + ANIMATION =====
let highlightedPieces = [];
let selectionListener = null;

function highlightMovablePieces(moves) {
  highlightedPieces = moves.map(m => m.pieceId);
  drawBoard(); // Redraw with highlights
}

function waitForPieceSelection(el, moves) {
  // Listen for canvas tap to select a piece
  const handler = (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const scaleX = boardSize / rect.width;
    const scaleY = boardSize / rect.height;
    const cx = x * scaleX;
    const cy = y * scaleY;

    // Find which movable piece was tapped
    for (const move of moves) {
      const pIdx = parseInt(move.pieceId.split('-')[0]);
      const pieceIdx = parseInt(move.pieceId.split('-')[1]);
      const piece = game.players[pIdx].pieces[pieceIdx];
      let pos;
      const slot = game.players[pIdx]?.boardSlot ?? pIdx;
      if (piece.pos === -1) pos = getHomeBasePosition(slot, pieceIdx, cellSize);
      else pos = getPiecePosition(piece.pos, slot, cellSize);
      if (!pos) continue;

      const dist = Math.sqrt((cx - pos.x) ** 2 + (cy - pos.y) ** 2);
      if (dist < cellSize * 0.6) {
        // Selected this piece!
        canvas.removeEventListener('click', handler);
        selectionListener = null;
        highlightedPieces = [];
        audio.play('click');
        juice.hapticLight();
        animateMove(el, move);
        return;
      }
    }
  };

  selectionListener = handler;
  canvas.addEventListener('click', handler);
}

// Smooth 60fps tween helper — runs a function with t=[0,1] each frame
function tween(duration, fn) {
  return new Promise(resolve => {
    const start = performance.now();
    function tick() {
      const elapsed = performance.now() - start;
      const t = Math.min(elapsed / duration, 1);
      fn(t);
      if (t < 1) requestAnimationFrame(tick);
      else resolve();
    }
    requestAnimationFrame(tick);
  });
}

// Easing functions
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
function easeOutBounce(t) {
  if (t < 1/2.75) return 7.5625*t*t;
  if (t < 2/2.75) { t -= 1.5/2.75; return 7.5625*t*t + 0.75; }
  if (t < 2.5/2.75) { t -= 2.25/2.75; return 7.5625*t*t + 0.9375; }
  t -= 2.625/2.75; return 7.5625*t*t + 0.984375;
}

async function animateMove(el, move) {
  const pIdx = parseInt(move.pieceId.split('-')[0]);
  const pieceIdx = parseInt(move.pieceId.split('-')[1]);
  const piece = game.players[pIdx].pieces[pieceIdx];
  const fromPos = piece.pos;
  const toPos = move.to;

  if (move.type === 'enter') {
    rules.applyMove(game, game.currentPlayer, move);
    audio.play('move', 'game');
    juice.hapticMedium();
    // Smooth bounce-in from home
    await tween(300, t => {
      const et = easeOutBounce(t);
      piece._bounceOffset = -cellSize * 0.7 * (1 - et);
    });
    piece._bounceOffset = 0;
    // Landing burst
    const enterSlot = game.players[pIdx]?.boardSlot ?? pIdx;
    const pos = getPiecePosition(0, enterSlot, cellSize);
    if (pos) {
      const rect = canvas.getBoundingClientRect();
      const sx = rect.left + (pos.x / boardSize) * rect.width;
      const sy = rect.top + (pos.y / boardSize) * rect.height;
      juice.burst?.(sx, sy, { count: 6, colors: [COLORS[enterSlot], '#fff'], size: 4, spread: 30, duration: 400 });
    }
    afterMove(el, move);
    return;
  }

  const steps = toPos - fromPos;
  if (steps <= 0 || steps > 6) {
    rules.applyMove(game, game.currentPlayer, move);
    afterMove(el, move);
    return;
  }

  // Smooth hop per step — each hop is a parabolic arc at 60fps
  const hopDuration = game.turboMode ? 120 : 200;
  const bounceHeight = cellSize * 0.6;
  for (let i = 1; i <= steps; i++) {
    piece.pos = fromPos + i;
    await tween(hopDuration, t => {
      // Parabolic arc: goes up then down
      const arc = -4 * bounceHeight * t * (t - 1);
      // Add slight squash on landing (last 20% of animation)
      const squash = t > 0.8 ? (t - 0.8) * 5 * 2 : 0;
      piece._bounceOffset = -arc + squash;
    });
    piece._bounceOffset = 0;
    audio.play('move', 'game');
  }

  // Landing effect on final square
  if (steps >= 3) {
    const landSlot = game.players[pIdx]?.boardSlot ?? pIdx;
    const pos = getPiecePosition(toPos, landSlot, cellSize);
    if (pos) {
      const rect = canvas.getBoundingClientRect();
      const sx = rect.left + (pos.x / boardSize) * rect.width;
      const sy = rect.top + (pos.y / boardSize) * rect.height;
      juice.burst?.(sx, sy, { count: 5, colors: [COLORS[landSlot], '#fff'], size: 4, spread: 25, duration: 350 });
    }
    juice.hapticLight();
  }

  // Apply via rules for capture logic
  piece.pos = fromPos;
  rules.applyMove(game, game.currentPlayer, move);
  afterMove(el, move);
}


function fireworkBurst(x, y, color) {
  const lighten = (hex, pct) => {
    const num = parseInt(hex.replace('#', ''), 16);
    const r = Math.min(255, ((num >> 16) & 0xFF) + pct);
    const g = Math.min(255, ((num >> 8) & 0xFF) + pct);
    const b = Math.min(255, (num & 0xFF) + pct);
    return `rgb(${r},${g},${b})`;
  };
  const colors = [color, lighten(color, 80), lighten(color, 140), '#fff'];
  // Wave 1 — fast tight burst
  juice.burst(x, y, { count: 16, colors, size: 7, spread: 90, duration: 700 });
  // Wave 2 — delayed wider burst
  setTimeout(() => juice.burst(x, y, { count: 20, colors, size: 9, spread: 160, duration: 1000, type: 'star' }), 200);
  // Wave 3 — sparkle trail
  setTimeout(() => juice.burst(x, y, { count: 10, colors: [color, '#fff'], size: 5, spread: 200, duration: 1200 }), 450);
}

function afterMove(el, move) {
  game.rolled = false;
  const mover = game.currentPlayer;
  const moverSlot = game.players[mover]?.boardSlot ?? mover;
  const isHumanMover = game.mode === 'local-multi' ? game.humanPlayers.includes(mover) : mover === myPlayerIndex;

  if (move.type === 'capture') {
    if (isHumanMover) {
      audio.play('capture', 'reward');
      juice.hapticSuccess();
      juice.confetti(window.innerWidth / 2, window.innerHeight / 2, 30);
      juice.starBurst(window.innerWidth / 2, window.innerHeight / 2, 10);
      juice.screenFlash('rgba(76,175,80,0.2)', 400);
    } else {
      const capturedMe = move._capturedPlayers && move._capturedPlayers.includes(myPlayerIndex);
      if (capturedMe) {
        audio.play('lose', 'game');
        juice.hapticError();
        juice.shake(el, 8, 400);
        juice.screenFlash('rgba(229,57,53,0.25)', 500);
      } else {
        audio.play('capture', 'game');
        juice.shake(el, 3, 150);
      }
    }
  } else if (move.type === 'finish') {
    if (isHumanMover) {
      audio.play('sfx_piece_home', 'reward');
      juice.hapticSuccess();
      const boardRect = canvas.getBoundingClientRect();
      fireworkBurst(boardRect.left + boardRect.width / 2, boardRect.top + boardRect.height / 2, COLORS[moverSlot]);
      juice.screenFlash(COLORS[moverSlot] + '22', 500);
    } else {
      audio.play('sfx_piece_home', 'game');
    }
    const panel = el.querySelector(`#pp-${mover}`);
    if (panel) panel.animate([{ background: 'rgba(76,175,80,0.3)' }, { background: 'transparent' }], { duration: 600 });
  }

  if (game.gameOver) {
    if (game.mode === 'live') syncLudoState(game.diceValue);
    endGame(el);
    return;
  }
  rules.nextTurn(game);
  updatePanels(el);
  drawBoard();

  if (game.mode === 'live') syncLudoState(game.diceValue);

  if (!isMyTurn()) handleNonPlayerTurn(el);
}
// ===== END PIECE SELECTION + ANIMATION =====

function startRenderLoop() {
  if (animFrame) return;
  const loop = () => {
    renderFrame();
    animFrame = requestAnimationFrame(loop);
  };
  animFrame = requestAnimationFrame(loop);
}

function stopRenderLoop() {
  if (animFrame) { cancelAnimationFrame(animFrame); animFrame = null; }
}

function renderFrame() {
  const cs = cellSize;
  if (!ctx || !boardCanvas) return;

  // Draw cached static board
  if (boardDirty) {
    drawStaticBoard();
    boardDirty = false;
  }
  clear(ctx, boardSize, boardSize);
  ctx.drawImage(boardCanvas, 0, 0);

  // === Ambient corner glow (animated, lightweight) ===
  if (game && !game.gameOver) {
    const cp = game.currentPlayer;
    const slot = game.players[cp]?.boardSlot ?? cp;
    const corners = [[0,boardSize],[0,0],[boardSize,0],[boardSize,boardSize]];
    const [gx, gy] = corners[slot];
    const ambGrad = ctx.createRadialGradient(gx, gy, 0, gx, gy, cs*5);
    ambGrad.addColorStop(0, COLORS[slot] + '20');
    ambGrad.addColorStop(1, 'transparent');
    ctx.fillStyle = ambGrad;
    ctx.fillRect(0, 0, boardSize, boardSize);
  }

  // Draw move preview ghosts
  if (highlightedPieces.length > 0 && validMoves && validMoves.length > 0) {
    const now = performance.now();
    const pulse = 0.2 + Math.sin(now * 0.004) * 0.1;
    validMoves.forEach(move => {
      const pIdx = parseInt(move.pieceId.split('-')[0]);
      const bSlot = game.players[pIdx]?.boardSlot ?? pIdx;
      const destPos = getPiecePosition(move.to, bSlot, cs);
      if (destPos) {
        const r = cs * 0.42;
        ctx.beginPath();
        ctx.arc(destPos.x, destPos.y, r, 0, Math.PI * 2);
        ctx.globalAlpha = pulse;
        ctx.fillStyle = COLORS[bSlot];
        ctx.fill();
        ctx.globalAlpha = 0.6;
        ctx.setLineDash([4, 3]);
        ctx.strokeStyle = COLORS[bSlot];
        ctx.lineWidth = 1.5;
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.globalAlpha = 1.0;
      }
    });
  }

  // Draw pieces with smooth interpolation
  drawPieces(cs);
}

function drawBoard() {
  boardDirty = true;
}

function drawStaticBoard() {
  const cs = cellSize;
  const sctx = boardCanvas.getContext('2d');
  sctx.clearRect(0, 0, boardSize, boardSize);

  // Background
  sctx.fillStyle = '#FAFAFA';
  sctx.fillRect(0, 0, boardSize, boardSize);

  // Home zones (dim inactive seats in <4 player games)
  const activeSlotsSet = new Set(game.activeSeats || [0,1,2,3]);
  [[0,0,9],[1,0,0],[2,9,0],[3,9,9]].forEach(([p,c,r]) => {
    const active = activeSlotsSet.has(p);
    sctx.globalAlpha = active ? 1.0 : 0.2;
    sctx.fillStyle = COLORS[p];
    sctx.fillRect(c*cs, r*cs, 6*cs, 6*cs);
    sctx.fillStyle = '#FAFAFA';
    const ins = cs*0.85;
    sctx.fillRect(c*cs+ins, r*cs+ins, 6*cs-ins*2, 6*cs-ins*2);
    sctx.strokeStyle = COLORS[p];
    sctx.lineWidth = 2.5;
    sctx.strokeRect(c*cs+ins+3, r*cs+ins+3, 6*cs-ins*2-6, 6*cs-ins*2-6);
    sctx.globalAlpha = 1.0;
  });

  // Cross paths
  sctx.fillStyle = '#fff';
  sctx.fillRect(6*cs,0,3*cs,6*cs);
  sctx.fillRect(6*cs,9*cs,3*cs,6*cs);
  sctx.fillRect(0,6*cs,6*cs,3*cs);
  sctx.fillRect(9*cs,6*cs,6*cs,3*cs);
  sctx.fillRect(6*cs,6*cs,3*cs,3*cs);

  // Grid lines
  sctx.strokeStyle = '#ddd';
  sctx.lineWidth = 0.5;
  for (let i = 0; i <= 15; i++) {
    if (i >= 6 && i <= 9) { sctx.beginPath(); sctx.moveTo(i*cs,0); sctx.lineTo(i*cs,boardSize); sctx.stroke(); sctx.beginPath(); sctx.moveTo(0,i*cs); sctx.lineTo(boardSize,i*cs); sctx.stroke(); }
  }
  for (let i = 0; i <= 6; i++) {
    sctx.beginPath(); sctx.moveTo(6*cs,i*cs); sctx.lineTo(9*cs,i*cs); sctx.stroke();
    sctx.beginPath(); sctx.moveTo(6*cs,(i+9)*cs); sctx.lineTo(9*cs,(i+9)*cs); sctx.stroke();
    sctx.beginPath(); sctx.moveTo(i*cs,6*cs); sctx.lineTo(i*cs,9*cs); sctx.stroke();
    sctx.beginPath(); sctx.moveTo((i+9)*cs,6*cs); sctx.lineTo((i+9)*cs,9*cs); sctx.stroke();
  }

  // Home columns (dim inactive)
  for (let p = 0; p < 4; p++) {
    sctx.globalAlpha = activeSlotsSet.has(p) ? 1.0 : 0.2;
    HOME_COLUMNS[p].forEach(([col,row]) => { sctx.fillStyle = COLORS_LIGHT[p]; sctx.fillRect(col*cs+1, row*cs+1, cs-2, cs-2); });
  }
  sctx.globalAlpha = 1.0;

  // Safe squares
  SAFE_SQUARES.forEach(idx => { const [col,row] = SHARED_PATH[idx]; sctx.fillStyle = '#FFF9C4'; sctx.fillRect(col*cs+1,row*cs+1,cs-2,cs-2); sctx.fillStyle = '#F9A825'; sctx.font = `${cs*0.45}px sans-serif`; sctx.textAlign = 'center'; sctx.textBaseline = 'middle'; sctx.fillText('★', col*cs+cs/2, row*cs+cs/2); });

  // Center triangles (dim inactive)
  const cx = 7.5*cs, cy = 7.5*cs, hs = 1.5*cs;
  const triPaths = [
    [0, [cx-hs,cy+hs], [cx+hs,cy+hs], [cx,cy]],  // Red bottom
    [1, [cx-hs,cy-hs], [cx-hs,cy+hs], [cx,cy]],  // Green left
    [2, [cx-hs,cy-hs], [cx+hs,cy-hs], [cx,cy]],  // Yellow top
    [3, [cx+hs,cy-hs], [cx+hs,cy+hs], [cx,cy]],  // Blue right
  ];
  triPaths.forEach(([p, a, b, c2]) => {
    sctx.globalAlpha = activeSlotsSet.has(p) ? 1.0 : 0.15;
    sctx.fillStyle = COLORS[p]; sctx.beginPath();
    sctx.moveTo(a[0],a[1]); sctx.lineTo(b[0],b[1]); sctx.lineTo(c2[0],c2[1]); sctx.closePath(); sctx.fill();
  });
  sctx.globalAlpha = 1.0;
  sctx.strokeStyle = '#fff'; sctx.lineWidth = 1.5;
  sctx.strokeRect(cx-hs, cy-hs, hs*2, hs*2);

  // Start position squares (dim inactive)
  const startColors = [[0,6,13,'#FFCDD2'],[1,1,6,'#C8E6C9'],[2,8,1,'#FFF9C4'],[3,13,8,'#BBDEFB']];
  startColors.forEach(([p,col,row,color]) => { sctx.globalAlpha = activeSlotsSet.has(p) ? 1.0 : 0.2; sctx.fillStyle = color; sctx.fillRect(col*cs+1, row*cs+1, cs-2, cs-2); });
  sctx.globalAlpha = 1.0;
}

function drawPieces(cs) {
  // Group pieces by their cell position to handle stacking
  const cellOccupants = new Map();
  game.players.forEach((player, pIdx) => {
    player.pieces.forEach((piece, pieceIdx) => {
      if (piece.finished) return;
      const bSlot = player.boardSlot ?? pIdx;
      let pos;
      if (piece.pos === -1) pos = getHomeBasePosition(bSlot, pieceIdx, cs);
      else pos = getPiecePosition(piece.pos, bSlot, cs);
      if (!pos) return;
      const key = `${Math.round(pos.x)},${Math.round(pos.y)}`;
      if (!cellOccupants.has(key)) cellOccupants.set(key, []);
      cellOccupants.get(key).push({ pIdx, pieceIdx, piece, pos });
    });
  });

  // Grid offsets for 1-4 pawns sharing a cell
  const STACK_OFFSETS = [
    [[0, 0]],
    [[-0.35, -0.3], [0.35, 0.3]],
    [[-0.35, -0.35], [0.35, -0.35], [0, 0.35]],
    [[-0.35, -0.35], [0.35, -0.35], [-0.35, 0.35], [0.35, 0.35]],
  ];

  cellOccupants.forEach((occupants) => {
    const count = occupants.length;
    const offsets = STACK_OFFSETS[Math.min(count, 4) - 1];
    const scale = count === 1 ? 1 : count === 2 ? 0.82 : 0.7;

    occupants.forEach((occ, idx) => {
      const { pIdx, pieceIdx, piece, pos } = occ;
      const pieceId = `${pIdx}-${pieceIdx}`;
      const isHighlighted = highlightedPieces.includes(pieceId);
      const r = cs * 0.42 * scale;
      const offset = offsets[idx % offsets.length];
      const ox = pos.x + offset[0] * cs;
      const oy = pos.y + offset[1] * cs;

      const bounceY = piece._bounceOffset || 0;
      const drawY = oy + bounceY;

      // Highlight ring (same for sprite or canvas pawn)
      if (isHighlighted) {
        ctx.beginPath(); ctx.arc(ox, drawY, r + 3, 0, Math.PI*2);
        ctx.strokeStyle = '#E4AC38'; ctx.lineWidth = 2.5; ctx.stroke();
        ctx.beginPath(); ctx.arc(ox, drawY, r + 6, 0, Math.PI*2);
        ctx.strokeStyle = 'rgba(228,172,56,0.3)'; ctx.lineWidth = 1.5; ctx.stroke();
      }
      // Shadow
      ctx.beginPath(); ctx.arc(ox, oy + 2, r, 0, Math.PI*2); ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.fill();

      const bSlot2 = game.players[pIdx]?.boardSlot ?? pIdx;
      const spriteImg = pawnImages[bSlot2];
      const pColor = COLORS[bSlot2];
      if (spriteImg) {
        const size = r * 2;
        const iw = spriteImg.naturalWidth || spriteImg.width;
        const ih = spriteImg.naturalHeight || spriteImg.height;
        const scale = Math.min(size / iw, size / ih);
        const dw = iw * scale;
        const dh = ih * scale;
        ctx.drawImage(spriteImg, ox - dw/2, drawY - dh/2, dw, dh);
      } else {
        ctx.beginPath(); ctx.arc(ox, drawY - r*0.15, r*0.65, 0, Math.PI*2); ctx.fillStyle = pColor; ctx.fill();
        ctx.beginPath(); ctx.ellipse(ox, drawY + r*0.4, r*0.8, r*0.4, 0, 0, Math.PI*2); ctx.fillStyle = pColor; ctx.fill();
        ctx.beginPath(); ctx.arc(ox, drawY - r*0.15, r*0.65, 0, Math.PI*2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
        ctx.beginPath(); ctx.ellipse(ox, drawY + r*0.4, r*0.8, r*0.4, 0, 0, Math.PI*2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
        ctx.beginPath(); ctx.arc(ox - r*0.2, drawY - r*0.35, r*0.18, 0, Math.PI*2); ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fill();
      }
    });
  });
}

function checkTurboMode(el) {
  if (game.turboMode) return;
  const elapsed = (Date.now() - game.startTime) / 60000;
  if (elapsed >= 12) {
    game.turboMode = true;
    const banner = document.createElement('div');
    banner.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(228,172,56,0.95);color:#000;padding:10px 24px;border-radius:12px;font-size:14px;font-weight:700;z-index:100;';
    banner.textContent = t('game.speed_mode');
    el.querySelector('#ludo-wrap')?.appendChild(banner);
    banner.animate([{opacity:0,transform:'translate(-50%,-50%) scale(0.5)'},{opacity:1,transform:'translate(-50%,-50%) scale(1)'},{opacity:0,transform:'translate(-50%,-50%) translateY(-20px)'}], {duration:2000}).onfinish = () => banner.remove();
    juice.hapticHeavy();
  }
}

function updatePanels(el) {
  checkTurboMode(el);
  for (let i = 0; i < game.numPlayers; i++) {
    const p = el.querySelector(`#pp-${i}`);
    if (p) {
      const wasActive = p.classList.contains('active');
      const isNowActive = i === game.currentPlayer;
      p.classList.toggle('active', isNowActive);
      if (game.players[i].finished) p.style.opacity = '0.5';
      if (wasActive && !isNowActive) {
        const md = el.querySelector(`#dice-${i}`);
        if (md) { setTimeout(() => { md.className = 'pp-dice'; md.style.animation = ''; }, 1200); }
      }
    }
  }
  const diceArea = el.querySelector('#dice-area');
  const btn = el.querySelector('#roll-btn');
  const diceBox = el.querySelector('#dice-box');
  const timerBar = el.querySelector('#turn-timer-bar');
  const turnStatus = el.querySelector('#turn-status');
  const myTurn = isMyTurn() && !game.players[game.currentPlayer].finished && !game.gameOver;
  const currentName = PLAYER_NAMES[game.currentPlayer] || '';
  const isBot = currentName.startsWith('Bot');

  // Dice area: always visible, style changes per state
  if (diceArea) {
    diceArea.style.opacity = '1';
    diceArea.style.pointerEvents = 'auto';
    if (!myTurn) {
      diceArea.style.background = 'linear-gradient(180deg,#0a0a18,#060612)';
    } else {
      diceArea.style.background = 'linear-gradient(180deg,#12122a,var(--bg-deep))';
    }
  }

  // Turn status badge at top of dice area
  if (turnStatus) {
    if (game.gameOver) {
      turnStatus.style.display = 'none';
    } else if (myTurn) {
      turnStatus.style.display = 'block';
      const slot = game.players[game.currentPlayer]?.boardSlot ?? game.currentPlayer;
      turnStatus.style.background = game.mode === 'local-multi'
        ? COLORS[slot]
        : 'linear-gradient(135deg,var(--gold),var(--warning))';
      turnStatus.style.color = game.mode === 'local-multi' && slot === 2 ? '#000' : '#000';
      turnStatus.textContent = game.mode === 'local-multi' ? t('game.turn_of', { name: currentName }) : t('game.your_turn');
      turnStatus.style.animation = 'fadeIn 0.3s ease-out';
    } else {
      turnStatus.style.display = 'block';
      turnStatus.style.background = 'rgba(255,255,255,0.08)';
      turnStatus.style.color = 'var(--text-secondary)';
      turnStatus.textContent = isBot ? t('game.bot_thinking', { name: currentName }) : t('game.turn_of', { name: currentName });
      turnStatus.style.animation = 'fadeIn 0.3s ease-out';
    }
  }

  // Dice box: always visible (shows last roll or opponent animation)
  if (diceBox) {
    diceBox.style.visibility = 'visible';
    if (!myTurn && !diceAnimating) {
      diceBox.style.opacity = '0.4';
      diceBox.style.filter = 'grayscale(0.5)';
    } else {
      diceBox.style.opacity = '1';
      diceBox.style.filter = 'none';
    }
  }

  // Roll button: contextual text, never hidden
  if (btn) {
    const canRoll = myTurn && !game.rolled && !diceAnimating;
    if (myTurn) {
      btn.style.visibility = 'visible';
      btn.disabled = !canRoll;
      btn.textContent = game.rolled ? t('game.choose_piece') : t('game.roll_dice');
      btn.style.opacity = canRoll ? '1' : '0.5';
      btn.style.background = 'linear-gradient(135deg,var(--gold),var(--warning))';
      btn.style.color = '#000';
      if (canRoll && !turnTimer) {
        startTurnTimer(el, () => handleRoll(el));
      }
    } else {
      btn.style.visibility = 'visible';
      btn.disabled = true;
      btn.textContent = isBot ? t('game.bot_thinking', { name: 'Bot' }) : t('game.opponent_turn');
      btn.style.opacity = '0.5';
      btn.style.background = 'rgba(255,255,255,0.06)';
      btn.style.color = 'var(--text-muted)';
    }
  }

  // Emote button: always enabled (social action)
  const emoteBtn = el.querySelector('#emote-btn');
  if (emoteBtn) {
    emoteBtn.style.pointerEvents = 'auto';
    emoteBtn.style.opacity = '1';
  }

  if (timerBar) timerBar.style.opacity = myTurn ? '1' : '0.15';
  if (!myTurn) {
    clearTurnTimer();
    if (diceAnimating && !game.rolled) diceAnimating = false;
  }
}

function startTurnTimer(el, onTimeout) {
  clearTurnTimer();
  turnTimerStart = performance.now();
  const fill = el.querySelector('#turn-timer-fill');
  if (fill) { fill.style.transition = 'none'; fill.style.transform = 'scaleX(1)'; }
  const timerBar = el.querySelector('#turn-timer-bar');
  if (timerBar) timerBar.style.opacity = '1';

  function tick() {
    const elapsed = performance.now() - turnTimerStart;
    const pct = Math.max(0, 1 - elapsed / TURN_TIMEOUT);
    if (fill) fill.style.transform = `scaleX(${pct})`;
    if (pct <= 0) {
      clearTurnTimer();
      onTimeout();
      return;
    }
    turnTimer = requestAnimationFrame(tick);
  }
  turnTimer = requestAnimationFrame(tick);
}

function clearTurnTimer() {
  if (turnTimer) { cancelAnimationFrame(turnTimer); turnTimer = null; }
}

function notifyMyTurn(el) {
  const diceArea = el.querySelector('#dice-area');
  if (diceArea) {
    diceArea.animate([
      { boxShadow: 'inset 0 0 0 0 rgba(228,172,56,0)' },
      { boxShadow: 'inset 0 0 30px 4px rgba(228,172,56,0.4)' },
      { boxShadow: 'inset 0 0 0 0 rgba(228,172,56,0)' }
    ], { duration: 800, easing: 'ease-out' });
  }
  juice.hapticLight();
}

function animateDice(el, playerIdx) {
  return new Promise(resolve => {
    const mainDice = el.querySelector('#dice-box');
    const miniDice = el.querySelector(`#dice-${playerIdx}`);
    const isMe = game.mode === 'local-multi' ? game.humanPlayers.includes(playerIdx) : playerIdx === myPlayerIndex;

    // Show mini dice in rolling state
    if (miniDice) {
      miniDice.className = 'pp-dice rolling';
      renderMiniDice(miniDice, 1);
    }

    // Show main dice for everyone (not just self)
    if (mainDice) {
      mainDice.style.visibility = 'visible';
      mainDice.style.opacity = '1';
    }

    audio.play('sfx_dice_shake', 'game');
    let count = 0;
    const maxFrames = game.turboMode ? 8 : 14;
    const shakeAnim = setInterval(() => {
      const randVal = Math.floor(Math.random() * 6) + 1;
      renderDiceFace(mainDice, randVal);
      if (miniDice) renderMiniDice(miniDice, randVal);
      const rx = (Math.random() - 0.5) * 16;
      const ry = (Math.random() - 0.5) * 16;
      const rot = (Math.random() - 0.5) * 30;
      mainDice.style.transform = `translate(${rx}px,${ry}px) rotate(${rot}deg) scale(0.9)`;
      count++;
      if (count > maxFrames) {
        clearInterval(shakeAnim);
        const dice = rules.rollDice(game, game.currentPlayer);
        game.diceValue = dice;
        renderDiceFace(mainDice, dice);
        if (miniDice) renderMiniDice(miniDice, dice);

        // Pop-in landing on main dice
        mainDice.style.transform = 'scale(1.3)';
        setTimeout(() => { mainDice.style.transform = 'scale(1)'; }, 180);

        // Mini dice lands with pop
        if (miniDice) {
          miniDice.className = 'pp-dice landed';
          miniDice.style.animation = 'dicePopIn 0.35s cubic-bezier(0.34,1.56,0.64,1)';
          if (dice === 6) miniDice.style.animation += ', diceSix 1s ease-in-out infinite';
        }

        audio.play('dice', 'game');
        juice.hapticHeavy();
        juice.shake(el.querySelector(isMe ? '#dice-area' : `#pp-${playerIdx}`), 4, 200);

        if (dice === 6) {
          audio.play('sfx_boost', 'reward');
          mainDice.style.boxShadow = '0 0 20px var(--gold), 0 4px 14px rgba(0,0,0,0.4)';
          setTimeout(() => { mainDice.style.boxShadow = '0 4px 14px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.8)'; }, 800);
          juice.hapticSuccess();
          const panel = el.querySelector(`#pp-${playerIdx}`);
          if (panel) panel.animate([{background:'rgba(228,172,56,0.3)'},{background:'transparent'}], {duration:600});
        }

        resolve(dice);
      }
    }, 55);
  });
}

function renderMiniDice(miniDice, value) {
  const dots = { 1:[0,0,0,0,1,0,0,0,0], 2:[0,0,1,0,0,0,1,0,0], 3:[0,0,1,0,1,0,1,0,0], 4:[1,0,1,0,0,0,1,0,1], 5:[1,0,1,0,1,0,1,0,1], 6:[1,0,1,1,0,1,1,0,1] };
  const pattern = dots[value] || dots[1];
  miniDice.style.display = 'grid';
  miniDice.style.gridTemplate = 'repeat(3,1fr)/repeat(3,1fr)';
  miniDice.style.padding = '3px';
  miniDice.innerHTML = pattern.map(d =>
    `<div style="width:5px;height:5px;border-radius:50%;background:${d ? 'var(--bg-card)' : 'transparent'};margin:auto;${d ? 'box-shadow:0 1px 1px rgba(0,0,0,0.2);' : ''}"></div>`
  ).join('');
}

async function handleExit(el) {
  if (game.gameOver) return;
  const confirmed = await modal.confirm(t('game.leave_confirm'), {
    title: t('game.leave_title'),
    icon: '🚪',
    confirmText: t('game.leave'),
    cancelText: t('game.stay'),
    danger: true
  });
  if (!confirmed) return;

  game.gameOver = true;
  clearTurnTimer();
  stopRenderLoop();
  audio.play('click');

  if (game.mode === 'live' && matchId) {
    net.post('ludo-match.php', { action: 'leave', match_id: matchId, player_index: myPlayerIndex }).catch(() => {});

    stopLudoPolling();
    if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
    localStorage.removeItem('el3ab_active_match');
  }

  scene.exitGameMode();
  scene.replace('ludo-result', {
    result: 'loss', resigned: true, mode: game.mode,
    numPlayers: game.numPlayers, seats: game.activeSeats,
    humanCount: game.humanPlayers?.length, difficulty: game.difficulty,
    playerNames: [...PLAYER_NAMES],
    playerColors: game.activeSeats.map(s => COLORS[s]),
  });
  bus.emit('game:ended', { gameKey: 'ludo', result: 'loss', resigned: true });
}

let endingGame = false;
function endGame(el) {
  if (endingGame) return;
  endingGame = true;
  game.gameOver = true;
  clearTurnTimer();
  stopRenderLoop();
  stopLudoPolling();
  if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
  if (matchId) localStorage.removeItem('el3ab_active_match');

  const myRank = game.winners.indexOf(myPlayerIndex);
  const place = myRank >= 0 ? myRank + 1 : game.numPlayers;
  const isLastPlace = place >= game.numPlayers || myRank === -1;
  const result = isLastPlace ? 'loss' : 'win';

  // Build ordered player IDs for rewards (1st → last)
  const players = game.humanPlayers ? PLAYER_NAMES : ['bot_0','bot_1','bot_2','bot_3'];
  const orderedWinners = game.winners.map(idx => {
    const name = PLAYER_NAMES[idx];
    if (name && name.startsWith('Bot')) return 'bot_' + idx;
    return store.get('player')?.id || 'unknown';
  });

  // Call server for rewards (fire and forget for bots, await for live)
  if (matchId || game.mode === 'live') {
    net.post('ludo-match.php', {
      action: 'complete',
      match_id: matchId,
      winners: orderedWinners,
      reason: 'normal'
    }).then(data => {
      if (data?.rewards) bus.emit('rewards:received', data.rewards);
    }).catch(() => {});
  }

  if (!isLastPlace) {
    juice.confetti(window.innerWidth / 2, window.innerHeight / 3, 50);
    juice.starBurst(window.innerWidth / 2, window.innerHeight / 3, 15);
    juice.hapticSuccess();
    audio.play('win', 'reward');
    juice.screenFlash('rgba(76,175,80,0.12)', 600);
    if (place === 1) {
      fireworkBurst(window.innerWidth / 2, window.innerHeight / 3, COLORS[game.players[myPlayerIndex]?.boardSlot ?? myPlayerIndex]);
    }
  } else {
    audio.play('lose', 'game');
    juice.hapticError();
    juice.screenFlash('rgba(229,57,53,0.12)', 500);
  }

  setTimeout(() => {
    scene.exitGameMode();
    scene.replace('ludo-result', {
      result, place, winners: game.winners, mode: game.mode,
      numPlayers: game.numPlayers, seats: game.activeSeats,
      humanCount: game.humanPlayers?.length, difficulty: game.difficulty,
      playerNames: [...PLAYER_NAMES],
      playerColors: game.activeSeats.map(s => COLORS[s]),
    });
    bus.emit('game:ended', { gameKey: 'ludo', result, place });
  }, 1500);
}

// ===== SOCIAL: EMOTES + PHRASES =====
const EMOTES = [
  { key: '😂', get label() { return t('emote.laugh'); } },
  { key: '😮', get label() { return t('emote.wow'); } },
  { key: '😡', get label() { return t('emote.angry'); } },
  { key: '👏', get label() { return t('emote.good_move'); } },
  { key: '🔥', get label() { return t('emote.hurry'); } },
  { key: '😢', get label() { return t('ludo.emote_sad'); } },
  { key: '💪', get label() { return t('ludo.emote_strong'); } },
  { key: '😎', get label() { return t('ludo.emote_cool'); } },
];

const PHRASES = [
  { key: 'gl', get text() { return t('ludo.phrase_gl'); } },
  { key: 'gg', text: 'GG!' },
  { key: 'hurry', get text() { return t('ludo.phrase_hurry'); } },
  { key: 'nice', get text() { return t('ludo.phrase_nice'); } },
  { key: 'oops', get text() { return t('ludo.phrase_oops'); } },
  { key: 'wow', get text() { return t('ludo.phrase_wow'); } },
];

let emoteCooldown = false;

function showEmotePanel(el) {
  const existing = el.querySelector('#ludo-emote-panel');
  if (existing) { existing.remove(); return; }

  const panel = document.createElement('div');
  panel.id = 'ludo-emote-panel';
  panel.style.cssText = `
    position:absolute;bottom:80px;left:50%;transform:translateX(-50%);
    background:var(--bg-card);border:1px solid rgba(228,172,56,0.15);
    border-radius:16px;padding:12px 14px;z-index:70;
    box-shadow:0 8px 30px rgba(0,0,0,0.7);
    animation:fadeIn 0.2s ease;display:flex;flex-direction:column;gap:10px;
    max-width:320px;width:90vw;
  `;

  panel.innerHTML = `
    <div style="display:flex;gap:4px;justify-content:center;flex-wrap:wrap;">
      ${EMOTES.map(e => `
        <button class="lep-emoji" data-key="${e.key}" title="${e.label}" style="font-size:24px;background:none;border:none;cursor:pointer;padding:6px;border-radius:8px;transition:transform 0.1s,background 0.1s;">
          ${e.key}
        </button>
      `).join('')}
    </div>
    <div style="height:1px;background:var(--border);"></div>
    <div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:center;">
      ${PHRASES.map(p => `
        <button class="lep-phrase" data-key="${p.key}" data-text="${p.text}" style="font-size:12px;font-weight:600;padding:6px 12px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:var(--text-light);cursor:pointer;transition:transform 0.1s,background 0.1s;">
          ${p.text}
        </button>
      `).join('')}
    </div>
  `;

  el.querySelector('#ludo-wrap')?.appendChild(panel);

  panel.querySelectorAll('.lep-emoji').forEach(btn => {
    btn.addEventListener('click', () => {
      if (emoteCooldown) return;
      sendSocialAction(el, btn.dataset.key, 'emoji');
      panel.remove();
    });
  });

  panel.querySelectorAll('.lep-phrase').forEach(btn => {
    btn.addEventListener('click', () => {
      if (emoteCooldown) return;
      sendSocialAction(el, btn.dataset.text, 'phrase');
      panel.remove();
    });
  });

  setTimeout(() => {
    const closeFn = (e) => {
      if (!panel.contains(e.target) && e.target.id !== 'emote-btn') {
        panel.remove();
        document.removeEventListener('pointerdown', closeFn);
      }
    };
    document.addEventListener('pointerdown', closeFn);
  }, 100);
}

function sendSocialAction(el, content, type) {
  emoteCooldown = true;
  setTimeout(() => { emoteCooldown = false; }, 3000);

  audio.play('sfx_emote', 'ui');
  showEmoteBubble(el, myPlayerIndex, content, type);

  if (game.mode === 'live' && matchId) {
    mp.sendEmote(matchId, 'ludo', content);
  }
}

function showEmoteBubble(el, senderIdx, content, type = 'emoji') {
  const panel = el.querySelector(`#pp-${senderIdx}`);
  if (!panel) return;

  const rect = panel.getBoundingClientRect();
  const wrapRect = el.getBoundingClientRect();

  const bubble = document.createElement('div');
  bubble.className = 'ludo-emote-bubble';

  if (type === 'phrase') {
    bubble.style.cssText = `
      position:absolute;z-index:60;pointer-events:none;
      left:${rect.left - wrapRect.left + rect.width / 2}px;
      top:${rect.top - wrapRect.top - 10}px;
      transform:translate(-50%,-100%);
      background:var(--bg-card);border:1px solid rgba(228,172,56,0.2);
      border-radius:12px;padding:6px 12px;
      font-size:13px;font-weight:700;color:var(--text-primary);
      white-space:nowrap;
      animation:phraseBubble 2.5s cubic-bezier(0.34,1.56,0.64,1) forwards;
    `;
    bubble.textContent = content;
  } else {
    bubble.style.cssText = `
      position:absolute;z-index:60;pointer-events:none;
      left:${rect.left - wrapRect.left + rect.width / 2}px;
      top:${rect.top - wrapRect.top - 10}px;
      transform:translate(-50%,-100%);
      font-size:36px;
      animation:emojiBubble 2s cubic-bezier(0.34,1.56,0.64,1) forwards;
    `;
    bubble.textContent = content;
  }

  el.appendChild(bubble);
  bubble.addEventListener('animationend', () => bubble.remove());
}

export function unmountGame() {
  stopRenderLoop();
  stopLudoPolling();
  clearTurnTimer();
  if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
  if (selectionListener && canvas) { canvas.removeEventListener('click', selectionListener); selectionListener = null; }
  mp.stopDisconnectWatch();
  localStorage.removeItem('el3ab_active_match');
}

