|
|
|
const socket = io({
|
|
reconnection: true,
|
|
reconnectionAttempts: 5,
|
|
reconnectionDelay: 1000
|
|
});
|
|
|
|
class Game {
|
|
constructor() {
|
|
|
|
this.state = {
|
|
gameId: null,
|
|
currentPhase: 'landing',
|
|
playerId: null,
|
|
players: [],
|
|
currentQuestion: null,
|
|
isRecording: false,
|
|
recordingTime: 30,
|
|
listeningTime: 60,
|
|
votingTime: 60,
|
|
recordings: new Map(),
|
|
votes: new Map(),
|
|
impostor: null
|
|
};
|
|
|
|
|
|
this.audioConfig = {
|
|
mediaRecorder: null,
|
|
audioChunks: [],
|
|
stream: null
|
|
};
|
|
|
|
|
|
this.initializeEventListeners();
|
|
this.bindUIElements();
|
|
}
|
|
|
|
|
|
initializeEventListeners() {
|
|
|
|
socket.on('connect', () => this.handleConnection());
|
|
socket.on('game_created', (data) => this.handleGameCreated(data));
|
|
socket.on('player_joined', (data) => this.handlePlayerJoined(data));
|
|
socket.on('round_start', (data) => this.handleRoundStart(data));
|
|
socket.on('round_result', (data) => this.handleRoundResult(data));
|
|
|
|
|
|
document.getElementById('start-button')?.addEventListener('click', () => this.startGame());
|
|
document.getElementById('add-player-button')?.addEventListener('click', () => this.addPlayer());
|
|
}
|
|
|
|
|
|
bindUIElements() {
|
|
|
|
const uiElements = {
|
|
gameContainer: document.getElementById('game-container'),
|
|
landingPage: document.getElementById('landing-page'),
|
|
setupPage: document.getElementById('setup-page'),
|
|
gamePage: document.getElementById('game-page'),
|
|
questionDisplay: document.getElementById('question-display'),
|
|
timerDisplay: document.getElementById('timer-display'),
|
|
recordButton: document.getElementById('record-button'),
|
|
playerList: document.getElementById('player-list'),
|
|
voteButtons: document.querySelectorAll('.vote-button')
|
|
};
|
|
|
|
|
|
this.ui = uiElements;
|
|
}
|
|
|
|
|
|
handleConnection() {
|
|
console.log('Connected to server');
|
|
this.showPage('landing');
|
|
}
|
|
|
|
|
|
async createGame() {
|
|
try {
|
|
socket.emit('create_game');
|
|
} catch (error) {
|
|
this.handleError('Failed to create game');
|
|
}
|
|
}
|
|
|
|
|
|
handleGameCreated(data) {
|
|
this.state.gameId = data.gameId;
|
|
this.showPage('setup');
|
|
}
|
|
|
|
|
|
async addPlayer() {
|
|
if (this.state.players.length >= 5) {
|
|
this.handleError('Maximum player limit reached');
|
|
return;
|
|
}
|
|
|
|
const playerName = prompt('Enter player name:');
|
|
if (!playerName) return;
|
|
|
|
socket.emit('join_game', {
|
|
gameId: this.state.gameId,
|
|
playerName: playerName
|
|
});
|
|
}
|
|
|
|
|
|
handlePlayerJoined(data) {
|
|
this.state.players.push({
|
|
id: data.playerId,
|
|
name: data.playerName
|
|
});
|
|
this.updatePlayerList();
|
|
}
|
|
|
|
|
|
updatePlayerList() {
|
|
if (!this.ui.playerList) return;
|
|
|
|
this.ui.playerList.innerHTML = '';
|
|
this.state.players.forEach(player => {
|
|
const playerElement = document.createElement('div');
|
|
playerElement.className = 'player-avatar';
|
|
playerElement.textContent = player.id;
|
|
this.ui.playerList.appendChild(playerElement);
|
|
});
|
|
}
|
|
|
|
|
|
async startGame() {
|
|
if (this.state.players.length < 3) {
|
|
this.handleError('Need at least 3 players to start');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/start_game', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
gameId: this.state.gameId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
this.handleRoundStart(data);
|
|
}
|
|
} catch (error) {
|
|
this.handleError('Failed to start game');
|
|
}
|
|
}
|
|
|
|
|
|
handleRoundStart(data) {
|
|
this.state.currentQuestion = data.question;
|
|
this.state.currentPhase = 'recording';
|
|
this.showPage('game');
|
|
this.updateQuestionDisplay();
|
|
this.startTimer(this.state.recordingTime, () => this.endRecordingPhase());
|
|
}
|
|
|
|
|
|
async startRecording() {
|
|
try {
|
|
this.audioConfig.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
this.audioConfig.mediaRecorder = new MediaRecorder(this.audioConfig.stream);
|
|
this.audioConfig.audioChunks = [];
|
|
|
|
this.audioConfig.mediaRecorder.ondataavailable = (event) => {
|
|
this.audioConfig.audioChunks.push(event.data);
|
|
};
|
|
|
|
this.audioConfig.mediaRecorder.onstop = () => {
|
|
this.submitRecording();
|
|
};
|
|
|
|
this.audioConfig.mediaRecorder.start();
|
|
this.state.isRecording = true;
|
|
this.updateRecordButton();
|
|
} catch (error) {
|
|
this.handleError('Failed to start recording');
|
|
}
|
|
}
|
|
|
|
|
|
stopRecording() {
|
|
if (this.audioConfig.mediaRecorder && this.state.isRecording) {
|
|
this.audioConfig.mediaRecorder.stop();
|
|
this.audioConfig.stream.getTracks().forEach(track => track.stop());
|
|
this.state.isRecording = false;
|
|
this.updateRecordButton();
|
|
}
|
|
}
|
|
|
|
|
|
async submitRecording() {
|
|
const audioBlob = new Blob(this.audioConfig.audioChunks, { type: 'audio/wav' });
|
|
const formData = new FormData();
|
|
formData.append('audio', audioBlob);
|
|
formData.append('gameId', this.state.gameId);
|
|
formData.append('playerId', this.state.playerId);
|
|
|
|
try {
|
|
const response = await fetch('/api/submit_recording', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
this.state.recordings.set(this.state.playerId, data.recordingUrl);
|
|
}
|
|
} catch (error) {
|
|
this.handleError('Failed to submit recording');
|
|
}
|
|
}
|
|
|
|
|
|
startTimer(duration, callback) {
|
|
let timeLeft = duration;
|
|
this.updateTimerDisplay(timeLeft);
|
|
|
|
this.timer = setInterval(() => {
|
|
timeLeft--;
|
|
this.updateTimerDisplay(timeLeft);
|
|
|
|
if (timeLeft <= 0) {
|
|
clearInterval(this.timer);
|
|
if (callback) callback();
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
|
|
updateTimerDisplay(timeLeft) {
|
|
if (this.ui.timerDisplay) {
|
|
const minutes = Math.floor(timeLeft / 60);
|
|
const seconds = timeLeft % 60;
|
|
this.ui.timerDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
}
|
|
}
|
|
|
|
|
|
endRecordingPhase() {
|
|
if (this.state.isRecording) {
|
|
this.stopRecording();
|
|
}
|
|
this.state.currentPhase = 'listening';
|
|
this.startListeningPhase();
|
|
}
|
|
|
|
|
|
startListeningPhase() {
|
|
this.showPage('listening');
|
|
this.loadRecordings();
|
|
this.startTimer(this.state.listeningTime, () => this.startVotingPhase());
|
|
}
|
|
|
|
|
|
async loadRecordings() {
|
|
|
|
|
|
}
|
|
|
|
|
|
startVotingPhase() {
|
|
this.state.currentPhase = 'voting';
|
|
this.showPage('voting');
|
|
this.startTimer(this.state.votingTime, () => this.endVotingPhase());
|
|
}
|
|
|
|
|
|
submitVote(votedPlayerId) {
|
|
socket.emit('submit_vote', {
|
|
gameId: this.state.gameId,
|
|
voterId: this.state.playerId,
|
|
votedPlayerId: votedPlayerId
|
|
});
|
|
}
|
|
|
|
|
|
handleRoundResult(data) {
|
|
this.showResults(data);
|
|
|
|
window.gameBackground?.addDramaticEffect('impostor_reveal');
|
|
}
|
|
|
|
|
|
showResults(data) {
|
|
this.showPage('results');
|
|
|
|
}
|
|
|
|
|
|
showPage(pageName) {
|
|
const pages = document.querySelectorAll('.game-page');
|
|
pages.forEach(page => {
|
|
page.classList.remove('active');
|
|
if (page.id === `${pageName}-page`) {
|
|
page.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
handleError(message) {
|
|
const errorElement = document.createElement('div');
|
|
errorElement.className = 'error-message';
|
|
errorElement.textContent = message;
|
|
this.ui.gameContainer.appendChild(errorElement);
|
|
setTimeout(() => errorElement.remove(), 3000);
|
|
}
|
|
}
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const game = new Game();
|
|
|
|
window.game = game;
|
|
});
|
|
|
|
export default Game; |