/** Start screen + lobby browser (DOM). */ export class GameMenu { constructor({ root, onOpenMode, onBack, onClaim, onLeaveSeat, onEnterMatch, onSpectate, }) { this.root = root; this.onOpenMode = onOpenMode; this.onBack = onBack; this.onClaim = onClaim; this.onLeaveSeat = onLeaveSeat; this.onEnterMatch = onEnterMatch; this.onSpectate = onSpectate; this.screen = 'start'; // start | lobby this.mode = null; this.username = null; this.spaceUrl = null; this.authError = null; this.playAllowed = true; this.selectedLobby = null; this.els = { start: root.querySelector('#menu-start'), lobby: root.querySelector('#menu-lobby'), lobbyTitle: root.querySelector('#lobby-title'), lobbyList: root.querySelector('#lobby-list'), lobbyMeta: root.querySelector('#lobby-meta'), userLabel: root.querySelector('#menu-user'), err: root.querySelector('#menu-error'), btnBack: root.querySelector('#btn-lobby-back'), btnEnter: root.querySelector('#btn-lobby-enter'), btnLeave: root.querySelector('#btn-lobby-leave'), }; root.querySelector('#btn-mode-sandbox')?.addEventListener('click', () => this._pickMode('sandbox')); root.querySelector('#btn-mode-mvp')?.addEventListener('click', () => this._pickMode('mvp')); this.els.btnBack?.addEventListener('click', () => { this.showStart(); this.onBack?.(); }); this.els.btnEnter?.addEventListener('click', () => this.onEnterMatch?.()); this.els.btnLeave?.addEventListener('click', () => this.onLeaveSeat?.()); } setIdentity({ username, spaceUrl, authError, playAllowed = true }) { this.username = username; this.spaceUrl = spaceUrl; this.authError = authError; this.playAllowed = playAllowed !== false; if (this.els.userLabel) { if (!username) { this.els.userLabel.textContent = authError || 'Not signed in — set HF_TOKEN in .env.local'; } else if (!this.playAllowed) { this.els.userLabel.textContent = `Spectator · ${username}`; } else if (String(username).startsWith('guest-')) { this.els.userLabel.textContent = `Guest · ${username}`; } else { this.els.userLabel.textContent = `Signed in as ${username}`; } } if (this.els.btnEnter) this.els.btnEnter.hidden = !this.playAllowed; if (this.els.btnLeave) this.els.btnLeave.hidden = !this.playAllowed; const sub = this.root.querySelector('.menu-sub'); if (sub) { sub.textContent = this.playAllowed ? 'Sandbox · MVP' : 'Spectate only — no HF login · play is local-only'; } for (const id of ['btn-mode-sandbox', 'btn-mode-mvp']) { const btn = this.root.querySelector(`#${id}`); if (btn) btn.hidden = !this.playAllowed; } if (!this.playAllowed && this.els.start) { this.showLobby('mvp'); this.onOpenMode?.('mvp'); } } show() { this.root.hidden = false; this.root.classList.add('menu-open'); this.showStart(); } hide() { this.root.hidden = true; this.root.classList.remove('menu-open'); } showStart() { this.screen = 'start'; this.mode = null; this.selectedLobby = null; if (this.els.start) this.els.start.hidden = false; if (this.els.lobby) this.els.lobby.hidden = true; if (this.els.lobbyList) this.els.lobbyList.innerHTML = ''; if (this.els.lobbyMeta) this.els.lobbyMeta.textContent = ''; if (this.els.lobbyTitle) this.els.lobbyTitle.textContent = 'Lobbies'; this._setError(''); } showLobby(mode) { this.screen = 'lobby'; this.mode = mode; if (this.els.start) this.els.start.hidden = true; if (this.els.lobby) this.els.lobby.hidden = false; if (this.els.lobbyTitle) { const titles = { sandbox: 'Sandbox lobbies', mvp: 'MVP lobbies' }; this.els.lobbyTitle.textContent = titles[mode] || mode; } } _pickMode(mode) { if (!this.username) { this._setError(this.authError || 'Set HF_TOKEN in .env.local'); return; } this._setError(''); this.showLobby(mode); this.onOpenMode?.(mode); } _setError(msg) { if (this.els.err) { this.els.err.textContent = msg || ''; this.els.err.hidden = !msg; } } _seatLabel(seatKey) { const m = String(seatKey).match(/^([SP])-(\d+)$/i); if (!m) return { team: null, label: seatKey, cls: '' }; return { team: null, label: m[2], cls: '' }; } _makeSeatBtn(mode, lobby, seat, user) { const meta = this._seatLabel(seat); const btn = document.createElement('button'); btn.type = 'button'; btn.className = `seat-btn ${meta.cls}${user ? ' filled' : ''}`.trim(); const locked = !this.playAllowed || !!user || lobby.status === 'live' || lobby.status === 'starting'; btn.disabled = locked; btn.title = locked ? (lobby.status === 'live' || lobby.status === 'starting' ? `${lobby.id} is ${lobby.status} — pick another lobby or wait` : (user ? `${user} is seated` : 'Spectate only')) : seat; btn.innerHTML = user ? `${meta.label}${user}` : meta.label; if (this.playAllowed) { btn.addEventListener('click', async () => { if (btn.disabled) return; try { this._setError(''); btn.disabled = true; btn.textContent = '…'; await this.onClaim?.(mode, lobby.id, seat); this.selectedLobby = lobby.id; } catch (err) { this._setError(err.message || String(err)); } }); } return btn; } renderBoard(board, mode, error = null) { const list = this.els.lobbyList; if (!list) return; if (error || !board) { list.innerHTML = `