// プレイ画面(スタブ): サーバから受け取ったテーブル状態を表示するだけ (function () { 'use strict'; const { Store, esc, chips, toast } = App; const $ = (id) => document.getElementById(id); const tableId = decodeURIComponent(location.pathname.split('/').pop()); const AVATAR_SVG = ''; let socket = null; let state = null; let clockOffset = 0; // serverNow - clientNow // ---------- 名前 ---------- function ensureName() { if (Store.name) return connect(); $('name-modal').hidden = false; $('name-input').focus(); } $('name-form').addEventListener('submit', (e) => { e.preventDefault(); const name = $('name-input').value.trim(); if (!name) return $('name-input').classList.add('invalid'); Store.name = name; $('name-modal').hidden = true; connect(); }); // ---------- 通信 ---------- function connect() { socket = App.connectSocket(); socket.on('connect', () => { $('conn-lost').hidden = true; socket.emit('table:join', { tableId, token: Store.token, name: Store.name }, (res) => { if (res && res.ok) return; if (res && res.error === 'NOT_FOUND') return showNotFound(); toast('参加できませんでした: ' + ((res && res.error) || '不明なエラー')); }); }); socket.on('disconnect', () => { $('conn-lost').hidden = false; }); socket.on('connect_error', () => { $('conn-lost').hidden = false; }); socket.on('table:state', (s) => { state = s; clockOffset = s.serverNow - Date.now(); render(); }); } function emit(event) { if (!socket) return; socket.emit(event, null, (res) => { if (res && !res.ok) toast(res.error === 'NOT_FOUND' ? 'テーブルが見つかりません' : res.error); }); } function showNotFound() { $('main').hidden = true; $('action-bar').hidden = true; $('not-found').hidden = false; if (socket) socket.disconnect(); } // ---------- 描画 ---------- function render() { const s = state; $('main').hidden = false; document.title = `${s.name} - Hold'em Night`; $('table-name').textContent = s.name; $('level').textContent = `Lv.${s.level}`; $('blinds').textContent = `SB: ${chips(s.smallBlind)} / BB: ${chips(s.bigBlind)}`; $('next-blinds').textContent = s.timer.enabled ? `次: ${chips(s.nextBlinds.smallBlind)} / ${chips(s.nextBlinds.bigBlind)}(${s.timer.intervalMin}分毎)` : 'ブラインドタイマーなし'; $('stack-info').textContent = `開始スタック ${chips(s.startingStack)}`; $('pot').textContent = chips(s.pot); $('phase').textContent = App.PHASE_LABELS[s.phase] || s.phase; $('hand-no').textContent = s.handNumber ? `Hand #${s.handNumber}` : ''; // ホスト操作 const players = s.seats.filter(Boolean).length; $('host-controls').hidden = !s.isHost; $('host-spacer').hidden = s.isHost; $('start').disabled = s.phase !== 'WAITING' || players < 2; $('start').title = players < 2 ? '2人以上で開始できます' : ''; $('next-phase').disabled = s.phase === 'WAITING'; $('pause').disabled = !s.started; $('pause').textContent = s.paused ? '再開' : '一時停止'; // 自分 const me = s.mySeat >= 0 ? s.seats[s.mySeat] : null; $('action-bar').hidden = !me; $('spectator-note').hidden = Boolean(me); $('leave').title = me ? '退席する' : 'ロビーへ戻る'; if (me) $('my-stack').textContent = chips(me.stack); renderSeats(s); renderTimer(); } // 着席者を楕円上に配置。自分が一番下、そこから時計回り。 function renderSeats(s) { const occupied = s.seats.filter(Boolean); const startIdx = Math.max(0, occupied.findIndex((p) => p.seat === s.mySeat)); const ordered = occupied.slice(startIdx).concat(occupied.slice(0, startIdx)); const n = ordered.length; const container = $('seats'); container.innerHTML = ordered .map((p) => { const classes = ['seat']; if (p.seat === s.mySeat) classes.push('me'); if (!p.connected) classes.push('offline'); const markers = [ p.seat === s.buttonSeat ? 'D' : '', p.seat === s.sbSeat ? 'SB' : '', p.seat === s.bbSeat ? 'BB' : '', ].join(''); return `