Download www/js/ui-menu.js from 1024m/HF-Racing: direct link, hf CLI and curl.
- Browser
- Download file 8.23 kB
-
https://huggingface.co/spaces/1024m/HF-Racing/resolve/main/www/js/ui-menu.js
- Command line
-
hf download hf://spaces/1024m/HF-Racing/www/js/ui-menu.js
-
curl -L -o ui-menu.js https://huggingface.co/spaces/1024m/HF-Racing/resolve/main/www/js/ui-menu.js
8.23 kB
| /** 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}<span class="seat-who">${user}</span>` | |
| : 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 = `<div class="lobby-card"><strong>Waiting for game server…</strong> | |
| <div class="lobby-seats" style="display:block;margin-top:0.4rem"> | |
| ${error || 'Lobby server not reachable.'} | |
| </div></div>`; | |
| this._setError(error || 'Lobby server not ready'); | |
| return; | |
| } | |
| this._setError(''); | |
| const key = mode === 'mvp' ? 'mvp' : 'sandbox'; | |
| const lobbies = board[key] || []; | |
| list.innerHTML = ''; | |
| for (const lobby of lobbies) { | |
| const card = document.createElement('div'); | |
| card.className = 'lobby-card'; | |
| card.dataset.id = lobby.id; | |
| const head = document.createElement('div'); | |
| head.className = 'lobby-card-head'; | |
| const cd = lobby.countdown != null ? ` · ${Math.ceil(lobby.countdown)}s` : ''; | |
| head.innerHTML = `<strong>${lobby.id}</strong><span>${lobby.filled}/${lobby.capacity} · ${lobby.status}${cd}</span>`; | |
| card.appendChild(head); | |
| const seats = document.createElement('div'); | |
| seats.className = 'lobby-seats'; | |
| for (const [seat, user] of Object.entries(lobby.seats || {})) { | |
| seats.appendChild(this._makeSeatBtn(mode, lobby, seat, user)); | |
| } | |
| if (lobby.status === 'live' || lobby.status === 'starting') { | |
| const spec = document.createElement('button'); | |
| spec.type = 'button'; | |
| spec.className = 'seat-btn spectate-btn'; | |
| spec.textContent = 'Spectate'; | |
| spec.addEventListener('click', async () => { | |
| try { | |
| this._setError(''); | |
| this.selectedLobby = lobby.id; | |
| await this.onSpectate?.(mode, lobby.id); | |
| } catch (err) { | |
| this._setError(err.message || String(err)); | |
| } | |
| }); | |
| seats.appendChild(spec); | |
| } | |
| card.appendChild(seats); | |
| list.appendChild(card); | |
| } | |
| if (this.els.lobbyMeta) { | |
| if (!this.playAllowed) { | |
| this.els.lobbyMeta.textContent = | |
| 'Spectate only — click Spectate on a live lobby. No HF login required.'; | |
| } else if (mode === 'mvp') { | |
| this.els.lobbyMeta.textContent = | |
| 'MVP: needs ≥3 players. 30s countdown starts when the seat roster is stable — join/leave resets the timer. Cars assigned randomly.'; | |
| } else { | |
| this.els.lobbyMeta.textContent = | |
| 'Sandbox: solo test drive. Starts when you take a seat. Default car: Porsche GT2 RS.'; | |
| } | |
| } | |
| } | |
| } | |