/* ============================================================
   斗地主 · 联机版客户端 online.jsx
   服务器权威: 本组件只渲染 + 发"意图"(BID/PLAY_CARDS/PASS)，
   一切状态来自服务端 S2C 事件(DEAL/TURN/MOVE_MADE/LANDLORD_SET/GAME_OVER/ROOM_STATE)。
   前端引擎 DDZ 仅用于本地合法性提示与“提示/托管”，不作结算依据。
   复用表现层组件: Card / MySeat / OpponentSeat / PlayZone / HoleCards / Bubble
   ============================================================ */
const { useState, useEffect, useRef } = React;
const DDZ = window.DDZ;
const SFX = window.SFX || { play() {}, setEnabled() {}, unlock() {} };

const FIELDS = [
  { id: 'novice', name: '新手场', base: 100 },
  { id: 'primary', name: '初级场', base: 1000 },
  { id: 'middle', name: '中级场', base: 5000 },
  { id: 'senior', name: '高级场', base: 20000 },
  { id: 'king', name: '王者场', base: 100000 },
];
const LABELS = {
  triple: '三张', triple_single: '三带一', triple_pair: '三带二', straight: '顺子',
  straight_pair: '连对', airplane: '飞机', airplane_single: '飞机', airplane_pair: '飞机',
  bomb: '💣 炸弹', rocket: '🚀 火箭',
};
const labelFor = (c) => c ? (LABELS[c.type] || '') : '';
const fmt = (n) => (n == null ? '—' : (n >= 10000 ? (n / 10000).toFixed(n % 10000 === 0 ? 0 : 1) + '万' : n.toLocaleString()));

/* 初始空对局 */
function emptyGame() {
  return {
    phase: 'waiting', mySeat: 0, base: 100, fieldName: '',
    players: [null, null, null],         // {seat,name,avatar,coins,count}
    myHand: [],
    current: null, leader: true, turnStart: 0, turnDeadline: 0, turnPhase: null,
    landlord: null, multiplier: 1, bombCount: 0,
    seatMoves: {}, lastCombo: null,
    bottom: [], bottomRevealed: false,
    bids: [null, null, null], bidStep: 0, bidStart: 0, mingpai: false,
    bubbles: {}, over: null,
  };
}

/* ============================ 顶层应用 ============================ */
function OnlineApp() {
  const netRef = useRef(null);
  const [screen, setScreen] = useState('connecting'); // connecting | lobby | matching | game
  const [me, setMe] = useState({ uid: null, nickname: '玩家', coins: null });
  const [match, setMatch] = useState({ field: FIELDS[0], filled: 0, need: 3 });
  const [game, setGame] = useState(emptyGame);
  const [toast, setToast] = useState(null);
  const gameRef = useRef(game); gameRef.current = game;
  const meRef = useRef(me); meRef.current = me;

  const flash = (msg, ms = 1600) => { setToast(msg); setTimeout(() => setToast(t => t === msg ? null : t), ms); };

  /* 建立连接 + 订阅事件（仅一次） */
  useEffect(() => {
    const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host;
    const net = new window.DDZNet(url);
    netRef.current = net;
    const offs = [];
    const sub = (t, fn) => offs.push(net.on(t, fn));

    sub('HELLO_OK', (d) => {
      setMe({ uid: d.uid, nickname: d.nickname, coins: d.coins });
      if (!d.roomId) setScreen(s => (s === 'connecting' ? 'lobby' : s));
    });
    sub('NET_CLOSED', () => flash('连接断开，正在重连…'));
    sub('NET_OPEN', () => {});
    sub('MATCHING', (d) => { setMatch(m => ({ ...m, filled: d.filled, need: d.need })); setScreen('matching'); });

    sub('DEAL', (d, msg) => {
      SFX.play('deal');
      setScreen('game');
      setGame(g => {
        const roster = d.players || (g.players[0] ? g.players.map(p => p && { ...p }) : null);
        const players = [0, 1, 2].map(seat => {
          const r = roster && roster.find(x => x.seat === seat);
          const prev = g.players[seat];
          return {
            seat, name: r ? r.name : (prev ? prev.name : '玩家' + seat),
            avatar: r ? r.avatar : (prev ? prev.avatar : '🙂'),
            coins: seat === d.seat ? meRef.current.coins : (prev ? prev.coins : null),
            count: d.counts ? d.counts[seat] : 17,
          };
        });
        const isMine = d.seat === g.mySeat || g.phase === 'waiting' || d.reason == null;
        return {
          ...emptyGame(), roomId: msg.roomId, base: g.base, fieldName: g.fieldName,
          mySeat: d.seat, players, myHand: d.hand.slice(),
          phase: 'dealing',
        };
      });
    });

    sub('BID_MADE', (d) => {
      SFX.play('bid');
      setGame(g => {
        const bids = [...g.bids]; bids[d.seat] = d.score;
        const highest = Math.max(0, ...bids.filter(x => x != null));
        const text = d.score > 0 ? (d.ming ? '明牌！' : `${d.score} 分`) : (highest > 0 ? '不抢' : '不叫');
        return { ...g, bids, bubbles: { ...g.bubbles, [d.seat]: { text, score: d.score > 0 } } };
      });
      clearBubble(d.seat);
    });

    sub('LANDLORD_SET', (d) => {
      SFX.play('landlord');
      setGame(g => {
        const players = g.players.map(p => p ? { ...p, count: d.counts ? d.counts[p.seat] : p.count } : p);
        // 地主拿底牌后手数变化；自己若是地主，手牌由随后的 DEAL(reason:landlord) 补齐
        return {
          ...g, phase: 'playing', landlord: d.seat, multiplier: d.multiplier,
          mingpai: d.mingpai, bottom: d.bottom || g.bottom, bottomRevealed: true, players,
          seatMoves: {}, lastCombo: null,
        };
      });
      const n = d.seat === gameRef.current.mySeat ? '你' : (gameRef.current.players[d.seat] || {}).name;
      flash(d.seat === gameRef.current.mySeat ? (d.mingpai ? '明牌！你是地主 👑' : '你是地主 👑') : `${n} 是地主`);
    });

    // 地主补发的整手牌（含底牌）
    // 复用 DEAL 通道，reason: landlord/mingpai
    sub('DEAL', (d) => {
      if (d.reason === 'landlord' || d.reason === 'mingpai') {
        setGame(g => (d.seat === g.mySeat ? { ...g, myHand: d.hand.slice() } : g));
      }
    });

    sub('TURN', (d) => {
      setGame(g => {
        let seatMoves = g.seatMoves, lastCombo = g.lastCombo;
        if (d.leader) { seatMoves = {}; lastCombo = null; }   // 新一轮领出，清台面
        return {
          ...g, phase: d.phase === 'bidding' ? 'bidding' : 'playing',
          current: d.seat, leader: !!d.leader, turnPhase: d.phase,
          turnStart: Date.now(), turnDeadline: d.deadline || 0, seatMoves, lastCombo,
        };
      });
    });

    sub('MOVE_MADE', (d) => {
      setGame(g => {
        const players = g.players.map(p => p ? { ...p, count: d.counts ? d.counts[p.seat] : p.count } : p);
        const seatMoves = { ...g.seatMoves };
        let lastCombo = g.lastCombo, myHand = g.myHand;
        if (d.pass) {
          seatMoves[d.seat] = { pass: true };
          SFX.play('pass');
        } else {
          const combo = DDZ.getCombo(d.cards);
          seatMoves[d.seat] = { cards: d.cards.slice().sort(DDZ.sortCards), label: labelFor(combo), anim: true };
          lastCombo = combo;
          if (combo && (combo.type === 'bomb' || combo.type === 'rocket')) SFX.play(combo.type); else SFX.play('play');
          if (d.seat === g.mySeat) { const ids = new Set(d.cards.map(c => c.id)); myHand = g.myHand.filter(c => !ids.has(c.id)); }
        }
        return { ...g, players, seatMoves, lastCombo, myHand, multiplier: d.multiplier != null ? d.multiplier : g.multiplier, bombCount: d.bombCount != null ? d.bombCount : g.bombCount };
      });
    });

    sub('GAME_OVER', (d) => {
      setGame(g => ({ ...g, phase: 'finished', over: d, current: null }));
      const g = gameRef.current;
      const myDelta = (d.deltas.find(x => x.seat === g.mySeat) || {}).delta || 0;
      const win = myDelta >= 0;
      SFX.play(win ? 'win' : 'lose');
      if (meRef.current.coins != null) setMe(m => ({ ...m, coins: Math.max(0, m.coins + myDelta) }));
    });

    sub('ROOM_STATE', (d) => {
      // 断线重连：用全量快照重建
      setScreen('game');
      setGame(g => rebuildFromSnapshot(g, d));
      flash('已重连，恢复对局');
    });

    sub('ERROR', (d) => { flash('⚠ ' + (d.msg || d.code)); SFX.play('invalid'); });

    function clearBubble(seat) {
      setTimeout(() => setGame(g => { const b = { ...g.bubbles }; delete b[seat]; return { ...g, bubbles: b }; }), 2200);
    }

    // 进入：游客连接
    SFX.unlock && SFX.unlock();
    net.connect('玩家' + Math.floor(1000 + Math.random() * 9000));

    return () => { offs.forEach(off => off()); net.close(); };
  }, []);

  const sendBubble = (text) => {
    const seat = gameRef.current.mySeat;
    setGame(g => ({ ...g, bubbles: { ...g.bubbles, [seat]: { text } } }));
    setTimeout(() => setGame(g => { const b = { ...g.bubbles }; if (b[seat] && b[seat].text === text) delete b[seat]; return { ...g, bubbles: b }; }), 2200);
  };

  const joinField = (field) => {
    setMatch({ field, filled: 1, need: 3 });
    setGame(g => ({ ...emptyGame(), base: field.base, fieldName: field.name }));
    netRef.current.send('JOIN_ROOM', { fieldId: field.id });
    setScreen('matching');
  };

  if (screen === 'connecting') return <Centered title="连接服务器中…" sub="ws 接入" />;
  if (screen === 'lobby') return <OnlineLobby me={me} onJoin={joinField} />;
  if (screen === 'matching') return <OnlineMatching me={me} match={match} onCancel={() => { netRef.current.send('LEAVE'); setScreen('lobby'); }} />;
  return (
    <React.Fragment>
      <OnlineGameTable game={game} me={me} net={netRef.current}
        onExit={() => { netRef.current.send('LEAVE'); setScreen('lobby'); setGame(emptyGame()); }}
        onRematch={() => joinField(match.field)}
        onBubble={sendBubble} flash={flash} />
      {toast && <div className="shell-toast">{toast}</div>}
    </React.Fragment>
  );
}

/* ============================ 牌桌 ============================ */
function OnlineGameTable({ game, me, net, onExit, onRematch, onBubble, flash }) {
  const [selected, setSelected] = useState(new Set());
  const [hintIds, setHintIds] = useState(new Set());
  const [scale, setScale] = useState(1);
  const [now, setNow] = useState(Date.now());
  const [tuoguan, setTuoguan] = useState(false);
  const [chatOpen, setChatOpen] = useState(false);
  const hintIdxRef = useRef(0);

  useEffect(() => {
    const fit = () => setScale(Math.min(window.innerWidth / 1440, window.innerHeight / 900));
    fit(); window.addEventListener('resize', fit); return () => window.removeEventListener('resize', fit);
  }, []);
  useEffect(() => { const iv = setInterval(() => setNow(Date.now()), 200); return () => clearInterval(iv); }, []);
  useEffect(() => { setSelected(new Set()); setHintIds(new Set()); hintIdxRef.current = 0; }, [game.current, game.phase]);

  const mySeat = game.mySeat;
  const myTurn = game.phase === 'playing' && game.current === mySeat;
  const myBidTurn = game.phase === 'bidding' && game.current === mySeat;
  const myLeader = game.leader;
  const prevCombo = myLeader ? null : game.lastCombo;

  /* 托管：到我出牌且开启托管 → 自动决策发意图 */
  useEffect(() => {
    if (!tuoguan) return;
    if (myTurn) {
      const t = setTimeout(() => {
        const ctx = { me: mySeat, landlord: game.landlord, counts: game.players.map(p => p ? p.count : 0), leaderSeat: null };
        let move = DDZ.aiMove(game.myHand, prevCombo, ctx);
        if (prevCombo && move && !DDZ.canBeat(DDZ.getCombo(move), prevCombo)) move = null;
        if (move) net.send('PLAY_CARDS', { cardIds: move.map(c => c.id) }); else net.send('PASS');
      }, 700);
      return () => clearTimeout(t);
    }
    if (myBidTurn) {
      const t = setTimeout(() => net.send('BID', { score: DDZ.aiBid ? DDZ.aiBid(game.myHand, highestBid(game)) : 0 }), 700);
      return () => clearTimeout(t);
    }
  }, [tuoguan, myTurn, myBidTurn, game.current]);

  const progress = (() => {
    if (!game.turnDeadline || game.turnDeadline <= game.turnStart) return myTurn || myBidTurn ? 1 : 0;
    return Math.max(0, Math.min(1, (game.turnDeadline - now) / (game.turnDeadline - game.turnStart)));
  })();

  const toggleCard = (id) => { if (!myTurn) return; SFX.play('select'); setHintIds(new Set()); setSelected(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }); };
  const dblCard = (card) => {
    if (!myTurn) return; SFX.play('select'); setHintIds(new Set());
    const same = game.myHand.filter(c => c.value === card.value).map(c => c.id);
    setSelected(s => { const all = same.every(id => s.has(id)); const n = new Set(s); same.forEach(id => all ? n.delete(id) : n.add(id)); return n; });
  };
  const selectedCards = () => game.myHand.filter(c => selected.has(c.id));
  const validSel = (() => { const c = DDZ.getCombo(selectedCards()); return c && (myLeader || DDZ.canBeat(c, prevCombo)); })();

  const onPlay = () => {
    const cards = selectedCards();
    const combo = DDZ.getCombo(cards);
    if (!combo) { SFX.play('invalid'); flash('牌型不合法'); return; }
    if (!myLeader && !DDZ.canBeat(combo, prevCombo)) { SFX.play('invalid'); flash('压不过上家'); return; }
    if (combo.type === 'bomb') onBubble('炸你！💣'); else if (combo.type === 'rocket') onBubble('接招吧！🚀');
    net.send('PLAY_CARDS', { cardIds: cards.map(c => c.id) });
    setSelected(new Set()); setHintIds(new Set());
  };
  const onPass = () => { if (myLeader) return; net.send('PASS'); setSelected(new Set()); setHintIds(new Set()); };
  const onHint = () => {
    const moves = DDZ.findAllMoves(game.myHand, prevCombo);
    if (!moves.length) { SFX.play('invalid'); flash(myLeader ? '没有可出的牌' : '没有能压过的牌'); return; }
    const move = moves[hintIdxRef.current % moves.length]; hintIdxRef.current++;
    SFX.play('select'); setSelected(new Set(move.map(c => c.id))); setHintIds(new Set(move.map(c => c.id)));
  };
  const onBid = (score, ming) => { SFX.play('bid'); net.send('BID', { score, ming: !!ming }); };

  const seatPlayer = (seat) => {
    const p = game.players[seat] || { name: '玩家' + seat, avatar: '🙂', coins: null, count: 0 };
    return { face: p.avatar, name: p.name, coins: p.coins == null ? 0 : p.coins, hand: { length: p.count } };
  };
  const rightSeat = (mySeat + 1) % 3, leftSeat = (mySeat + 2) % 3;
  const highest = highestBid(game);
  const myHand = game.myHand;
  const step = myHand.length > 1 ? Math.min(54, (1020 - 78) / (myHand.length - 1)) : 0;

  const over = game.over;
  const myDelta = over ? ((over.deltas.find(x => x.seat === mySeat) || {}).delta || 0) : 0;
  const meLandlord = game.landlord === mySeat;

  return (
    <div id="table" style={{ transform: `scale(${scale})` }}>
      <div className="felt" /><div className="center-emblem">斗地主</div><div className="rail" />

      <div className="topbar">
        <div className="room"><div className="badge">斗</div><div className="meta"><b>{game.fieldName || '联机对战'}</b><span>底分 {fmt(game.base)} · 联机</span></div></div>
        <div className="stats">
          <div className="stat"><span className="k">底　分</span><span className="v">{fmt(game.base)}</span></div>
          <div className="stat mult"><span className="k">倍　数</span><span className="v">×{game.multiplier}</span></div>
          {game.bombCount > 0 && <div className="stat bomb"><span className="k">炸　弹</span><span className="v">{game.bombCount}</span></div>}
          <div className="stat"><span className="k">底　池</span><span className="v">{fmt(game.base * game.multiplier)}</span></div>
        </div>
        <div className="topbtns">
          {game.mingpai && <div className="ming-flag">明牌</div>}
          <div className="icon-btn" title="退出" onClick={onExit}>✕</div>
        </div>
      </div>

      <HoleCards cards={game.bottom} revealed={game.bottomRevealed} />

      <OpponentSeat side="left" player={seatPlayer(leftSeat)} active={game.current === leftSeat && game.phase !== 'finished'} timer={progress} isLandlord={game.landlord === leftSeat} />
      <OpponentSeat side="right" player={seatPlayer(rightSeat)} active={game.current === rightSeat && game.phase !== 'finished'} timer={progress} isLandlord={game.landlord === rightSeat} />
      <MySeat player={seatPlayer(mySeat)} active={game.current === mySeat && game.phase === 'playing'} timer={progress} isLandlord={game.landlord == null ? null : meLandlord} />

      <PlayZone pos="left" move={game.seatMoves[leftSeat]} />
      <PlayZone pos="right" move={game.seatMoves[rightSeat]} />
      <PlayZone pos="me" move={game.seatMoves[mySeat]} />

      {game.bubbles[leftSeat] && <Bubble text={game.bubbles[leftSeat].text} score={game.bubbles[leftSeat].score} pos="left" />}
      {game.bubbles[rightSeat] && <Bubble text={game.bubbles[rightSeat].text} score={game.bubbles[rightSeat].score} pos="right" />}
      {game.bubbles[mySeat] && <Bubble text={game.bubbles[mySeat].text} score={game.bubbles[mySeat].score} pos="me" />}

      <div className="my-hand">
        {myHand.map((c, i) => (
          <Card key={c.id} card={c} selected={selected.has(c.id)} hintable={hintIds.has(c.id)}
            onClick={() => toggleCard(c.id)} onDoubleClick={() => dblCard(c)}
            style={{ marginLeft: i ? step - 78 : 0, zIndex: i }} />
        ))}
      </div>

      {myBidTurn && (
        <div className="bid-panel">
          <div className="title">{highest > 0 ? '是否抢地主？' : '请叫地主'}</div>
          <div className="bid-row">
            <button className="bid-btn pass" onClick={() => onBid(0)}>{highest > 0 ? '不抢' : '不叫'}</button>
            {[1, 2, 3].map(s => (
              <button key={s} className="bid-btn score" disabled={s <= highest} onClick={() => onBid(s)}>{s} 分<small>{['', '×1', '×2', '×3'][s]}</small></button>
            ))}
            <button className="bid-btn ming" disabled={highest >= 3} onClick={() => onBid(3, true)}>明牌<small>×2 风险</small></button>
          </div>
        </div>
      )}

      {game.phase === 'playing' && (
        <div className="actions">
          {myTurn && !tuoguan ? (
            <React.Fragment>
              <button className="btn ghost" onClick={onPass} disabled={myLeader}>不　出</button>
              <button className="btn ghost" onClick={onHint}>提　示</button>
              <button className="btn primary" onClick={onPlay} disabled={!validSel}>出　牌</button>
              <button className="btn ghost" onClick={() => setTuoguan(true)}>托　管</button>
            </React.Fragment>
          ) : tuoguan ? (
            <button className="btn danger" onClick={() => setTuoguan(false)}>取消托管</button>
          ) : (
            <div className="move-label" style={{ fontSize: 14, padding: '12px 0' }}>
              {game.current != null ? `${seatPlayer(game.current).name} 出牌中…` : '等待中…'}
            </div>
          )}
        </div>
      )}

      {game.phase !== 'finished' && (
        <div className="chat-dock">
          {chatOpen && <div className="quick-list">{['快点出牌啦~', '你的牌打得太好了!', '哈哈，炸你个措手不及', '就这？再来！'].map((q, i) => (<button key={i} onClick={() => { onBubble(q); setChatOpen(false); }}>{q}</button>))}</div>}
          <div className="chat-toggle" onClick={() => setChatOpen(o => !o)}>💬</div>
        </div>
      )}

      {over && (
        <div className="result-overlay">
          <div className="result-card">
            <div className={`head ${myDelta >= 0 ? 'win' : 'lose'}`}>{myDelta >= 0 ? '胜　利' : '失　败'}</div>
            {over.spring && <div className="spring-tag">{over.spring === 'spring' ? '🌸 春天 ×2' : '🍂 反春 ×2'}</div>}
            <div className={`delta ${myDelta >= 0 ? 'up' : 'down'}`}>{myDelta >= 0 ? '+' : ''}{myDelta.toLocaleString()}</div>
            <div className="result-breakdown">
              <div className="bd-row"><span className="bk">底分</span><span className="bv">{fmt(over.base)}</span></div>
              <div className="bd-row"><span className="bk">总倍数</span><span className="bv">×{over.multiplier}</span></div>
              {over.bombCount > 0 && <div className="bd-row"><span className="bk">炸弹/火箭</span><span className="bv">{over.bombCount}</span></div>}
              <div className="bd-row"><span className="bk">身份</span><span className="bv">{meLandlord ? '地主 ×2' : '农民 ×1'}</span></div>
              <div className="bd-row total"><span className="bk">{over.winnerSide === 'landlord' ? '地主获胜' : '农民获胜'} · 总计</span><span className="bv">{myDelta >= 0 ? '+' : ''}{myDelta.toLocaleString()}</span></div>
            </div>
            <div className="result-actions">
              <button className="btn ghost" onClick={onExit}>返回大厅</button>
              <button className="btn primary" onClick={onRematch}>再　来　一　局</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ============================ 大厅 / 匹配 / 占位 ============================ */
function OnlineLobby({ me, onJoin }) {
  return (
    <div className="screen hall lobby">
      <div className="lobby-header">
        <div className="player-card"><div className="avatar-ring">😎</div>
          <div className="pc-info"><div className="pc-name">{me.nickname}</div>
            <div className="pc-rank">🪙 {fmt(me.coins)} · 联机大厅</div></div></div>
      </div>
      <div className="lobby-body">
        <div className="mode-hero accent-green" onClick={() => onJoin(FIELDS[0])}>
          <div className="watermark">♠</div><div className="tagpill">真 人 对 战</div>
          <h2>经典斗地主 · 联机</h2><p>服务器权威发牌 · 不足 AI 补位</p><div className="start">快 速 匹 配</div>
        </div>
        <div className="mode-grid">
          {FIELDS.map((f, i) => (
            <div key={f.id} className="mode-card accent-gold" style={{ animationDelay: `${i * 60}ms` }} onClick={() => onJoin(f)}>
              <div className="mc-suit">♦</div><h3>{f.name}</h3><p>底分 {fmt(f.base)}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function OnlineMatching({ me, match, onCancel }) {
  const seats = [{ name: me.nickname, face: '😎' }, null, null];
  for (let i = 1; i < match.filled && i < 3; i++) seats[i] = { name: '玩家', face: '🙂' };
  return (
    <div className="screen matching">
      <div className="match-inner">
        <div className="match-title">正在匹配对手</div>
        <div className="match-field">{match.field.name} · 底分 {fmt(match.field.base)}</div>
        <div className="match-seats">
          {seats.map((s, i) => (
            <div className="match-seat" key={i}><div className={`match-av ${s ? 'filled' : ''}`}>{s ? s.face : '?'}</div>
              <div className={`nm ${s ? 'on' : ''}`}>{s ? s.name : '等待中'}</div></div>
          ))}
        </div>
        <div className="match-tip">服务器撮合中（{match.filled}/{match.need}）<span className="dots" /></div>
        <div className="match-cancel"><button className="btn ghost" onClick={onCancel}>取消匹配</button></div>
      </div>
    </div>
  );
}

function Centered({ title, sub }) {
  return <div className="screen matching"><div className="match-inner"><div className="match-title">{title}</div><div className="match-tip">{sub}<span className="dots" /></div></div></div>;
}

/* ---------- 辅助 ---------- */
function highestBid(game) { return Math.max(0, ...game.bids.filter(x => x != null)); }
function rebuildFromSnapshot(g, d) {
  const players = [0, 1, 2].map(seat => {
    const r = (d.players || []).find(x => x.seat === seat) || {};
    return { seat, name: r.name || ('玩家' + seat), avatar: r.avatar || '🙂', coins: seat === d.yourSeat ? g.players[g.mySeat]?.coins ?? null : null, count: d.counts ? d.counts[seat] : 0 };
  });
  const lastCombo = d.lastMove ? DDZ.getCombo(d.lastMove.cards) : null;
  const seatMoves = {};
  if (d.lastMove) seatMoves[d.lastMove.seat] = { cards: d.lastMove.cards.slice().sort(DDZ.sortCards), label: labelFor(lastCombo), anim: false };
  return {
    ...emptyGame(), roomId: d.roomId, base: d.base, fieldName: g.fieldName, mySeat: d.yourSeat,
    players, myHand: d.hand ? d.hand.slice() : [], phase: d.phase,
    current: d.current, leader: !!d.leader, landlord: d.landlord, multiplier: d.multiplier || 1, bombCount: d.bombCount || 0,
    bids: d.bids || [null, null, null], bidStep: d.bidStep || 0, bidStart: d.bidStart || 0,
    bottom: d.bottom || [], bottomRevealed: !!d.bottom, seatMoves, lastCombo,
    turnStart: Date.now(), turnDeadline: 0,
  };
}

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