// Golf Card Game - Replay Viewer class ReplayViewer { constructor() { this.frames = []; this.metadata = null; this.currentFrame = 0; this.isPlaying = false; this.playbackSpeed = 1.0; this.playInterval = null; this.gameId = null; this.shareCode = null; this.initElements(); this.bindEvents(); } initElements() { this.replayScreen = document.getElementById('replay-screen'); this.replayTitle = document.getElementById('replay-title'); this.replayMeta = document.getElementById('replay-meta'); this.replayBoard = document.getElementById('replay-board'); this.eventDescription = document.getElementById('replay-event-description'); this.controlsContainer = document.getElementById('replay-controls'); this.frameCounter = document.getElementById('replay-frame-counter'); this.timelineSlider = document.getElementById('replay-timeline'); this.speedSelect = document.getElementById('replay-speed'); // Control buttons this.btnStart = document.getElementById('replay-btn-start'); this.btnPrev = document.getElementById('replay-btn-prev'); this.btnPlay = document.getElementById('replay-btn-play'); this.btnNext = document.getElementById('replay-btn-next'); this.btnEnd = document.getElementById('replay-btn-end'); // Action buttons this.btnShare = document.getElementById('replay-btn-share'); this.btnExport = document.getElementById('replay-btn-export'); this.btnBack = document.getElementById('replay-btn-back'); } bindEvents() { if (this.btnStart) this.btnStart.onclick = () => this.goToFrame(0); if (this.btnEnd) this.btnEnd.onclick = () => this.goToFrame(this.frames.length - 1); if (this.btnPrev) this.btnPrev.onclick = () => this.prevFrame(); if (this.btnNext) this.btnNext.onclick = () => this.nextFrame(); if (this.btnPlay) this.btnPlay.onclick = () => this.togglePlay(); if (this.timelineSlider) { this.timelineSlider.oninput = (e) => { this.goToFrame(parseInt(e.target.value)); }; } if (this.speedSelect) { this.speedSelect.onchange = (e) => { this.playbackSpeed = parseFloat(e.target.value); if (this.isPlaying) { this.stopPlayback(); this.startPlayback(); } }; } if (this.btnShare) { this.btnShare.onclick = () => this.showShareDialog(); } if (this.btnExport) { this.btnExport.onclick = () => this.exportGame(); } if (this.btnBack) { this.btnBack.onclick = () => this.hide(); } // Keyboard controls document.addEventListener('keydown', (e) => { if (!this.replayScreen || !this.replayScreen.classList.contains('active')) return; switch (e.key) { case 'ArrowLeft': e.preventDefault(); this.prevFrame(); break; case 'ArrowRight': e.preventDefault(); this.nextFrame(); break; case ' ': e.preventDefault(); this.togglePlay(); break; case 'Home': e.preventDefault(); this.goToFrame(0); break; case 'End': e.preventDefault(); this.goToFrame(this.frames.length - 1); break; } }); } async loadReplay(gameId) { this.gameId = gameId; this.shareCode = null; try { const token = localStorage.getItem('authToken'); const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; const response = await fetch(`/api/replay/game/${gameId}`, { headers }); if (!response.ok) { throw new Error('Failed to load replay'); } const data = await response.json(); this.frames = data.frames; this.metadata = data.metadata; this.currentFrame = 0; this.show(); this.render(); this.updateControls(); } catch (error) { console.error('Failed to load replay:', error); this.showError('Failed to load replay. You may not have permission to view this game.'); } } async loadSharedReplay(shareCode) { this.shareCode = shareCode; this.gameId = null; try { const response = await fetch(`/api/replay/shared/${shareCode}`); if (!response.ok) { throw new Error('Replay not found or expired'); } const data = await response.json(); this.frames = data.frames; this.metadata = data.metadata; this.gameId = data.game_id; this.currentFrame = 0; // Update title with share info if (data.title) { this.replayTitle.textContent = data.title; } this.show(); this.render(); this.updateControls(); } catch (error) { console.error('Failed to load shared replay:', error); this.showError('Replay not found or has expired.'); } } show() { // Hide other screens document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); this.replayScreen.classList.add('active'); // Update title if (!this.shareCode && this.metadata) { this.replayTitle.textContent = 'Game Replay'; } // Update meta if (this.metadata) { const players = this.metadata.players.join(' vs '); const duration = this.formatDuration(this.metadata.duration); const rounds = `${this.metadata.total_rounds} hole${this.metadata.total_rounds > 1 ? 's' : ''}`; this.replayMeta.innerHTML = `${players} | ${rounds} | ${duration}`; } } hide() { this.stopPlayback(); this.replayScreen.classList.remove('active'); // Return to lobby document.getElementById('lobby-screen').classList.add('active'); } render() { if (!this.frames.length) return; const frame = this.frames[this.currentFrame]; const state = frame.state; this.renderBoard(state); this.renderEventInfo(frame); this.updateTimeline(); } renderBoard(state) { const currentPlayerId = state.current_player_id; // Build HTML for all players let html = '
${this.escapeHtml(message)}