// Main Application - Ties everything together let localGame = null; let renderer = null; let lastTime = 0; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { // Connect to server network.connect(); // Initialize renderer renderer = new TetrisRenderer('battle-grid'); // Setup network listeners setupNetworkListeners(); // Setup keyboard controls setupKeyboardControls(); }); function setupNetworkListeners() { // Player joined lobby network.setListener('player-joined', (player) => { ui.updatePlayerList(network.getAllPlayers()); // Show lobby screen if we're on the login screen if (ui.screens.room.classList.contains('active')) { ui.showScreen('lobby'); } }); // Player left lobby network.setListener('player-left', (playerId) => { ui.updatePlayerList(network.getAllPlayers()); }); // Game started network.setListener('game-started', (players, states) => { ui.showScreen('game'); ui.displays.gameRoomName.textContent = 'GLOBAL LOBBY'; // Clear old boards renderer.clearAll(); // Create boards for all players states.forEach((state) => { const player = network.getPlayer(state.playerId); renderer.createPlayerBoard(state.playerId, player.name); // Initialize local game for current player if (state.playerId === network.currentPlayerId) { localGame = new TetrisGame(state.playerId); localGame.loadState(state); } }); // Set up battle grid layout updateBattleGridLayout(players.length); // Start game loop lastTime = performance.now(); requestAnimationFrame(gameLoop); }); // State update during game network.setListener('state-update', (states) => { // Update local game if it's our state const localState = states.find(s => s.playerId === network.currentPlayerId); if (localState) { localGame.loadState(localState); } // Check for game over const allStates = network.getAllGameStates(); const activePlayers = Object.values(allStates).filter(s => !s.eliminated); if (activePlayers.length <= 1) { endGame(allStates); } }); // Game over network.setListener('game-over', (data) => { endGame(data.states); }); // Forced spectator - game already in progress network.setListener('forced-spectator', ({ states, players }) => { ui.showScreen('game'); ui.showSpectatorMode(); // Clear old boards renderer.clearAll(); // Create boards for all players (spectator can see everyone) states.forEach((state) => { const player = network.getPlayer(state.playerId); renderer.createPlayerBoard(state.playerId, player.name); }); // Set up battle grid layout updateBattleGridLayout(players.length); // Start game loop for viewing lastTime = performance.now(); requestAnimationFrame(gameLoop); }); // Spectator joined network.setListener('spectator-joined', (spectator) => { ui.updateSpectatorList(network.getAllSpectators()); }); // Spectator left network.setListener('spectator-left', (spectatorId) => { ui.updateSpectatorList(network.getAllSpectators()); }); } function setupKeyboardControls() { document.addEventListener('keydown', (e) => { // Spectators cannot control the game if (network.isSpectator) return; if (ui.screens.game.classList.contains('active') && localGame) { switch (e.key) { case 'ArrowLeft': network.sendMove('left'); e.preventDefault(); break; case 'ArrowRight': network.sendMove('right'); e.preventDefault(); break; case 'ArrowDown': network.sendDrop(); e.preventDefault(); break; case 'ArrowUp': network.sendRotate(); e.preventDefault(); break; case ' ': network.sendHardDrop(); e.preventDefault(); break; case 'c': case 'C': network.sendHold(); e.preventDefault(); break; } } }); // Touch controls for mobile const btnLeft = document.getElementById('btn-left'); const btnRight = document.getElementById('btn-right'); const btnDown = document.getElementById('btn-down'); const btnRotate = document.getElementById('btn-rotate'); const btnDrop = document.getElementById('btn-drop'); const btnHold = document.getElementById('btn-hold'); const handleTouch = (e, action) => { e.preventDefault(); e.stopPropagation(); // Spectators cannot control the game if (network.isSpectator) return; if (ui.screens.game.classList.contains('active')) { action(); } }; // Use pointerdown for better touch response btnLeft.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendMove('left'))); btnRight.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendMove('right'))); btnDown.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendDrop())); btnRotate.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendRotate())); btnDrop.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHardDrop())); btnHold.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHold())); // Prevent double-tap zoom btnLeft.addEventListener('touchstart', (e) => e.preventDefault()); btnRight.addEventListener('touchstart', (e) => e.preventDefault()); btnDown.addEventListener('touchstart', (e) => e.preventDefault()); btnRotate.addEventListener('touchstart', (e) => e.preventDefault()); btnDrop.addEventListener('touchstart', (e) => e.preventDefault()); btnHold.addEventListener('touchstart', (e) => e.preventDefault()); } function updateBattleGridLayout(playerCount) { ui.displays.battleGrid.classList.remove('single-player', 'two-players', 'multi-player'); if (playerCount === 1) { ui.displays.battleGrid.classList.add('single-player'); } else if (playerCount === 2) { ui.displays.battleGrid.classList.add('two-players'); } else { ui.displays.battleGrid.classList.add('multi-player'); } } function endGame(states) { // Find winner const activePlayers = Object.values(states).filter(s => !s.eliminated); const eliminatedPlayers = Object.values(states).filter(s => s.eliminated); let winner = null; let scores = {}; if (activePlayers.length === 1) { const winnerState = activePlayers[0]; const winnerPlayer = network.getPlayer(winnerState.playerId); winner = winnerPlayer.name; } // Build scores list with garbage info Object.values(states).forEach(state => { const player = network.getPlayer(state.playerId); if (player) { scores[player.name] = { score: state.score, garbageReceived: (state.garbageReceived || []).length, eliminated: state.eliminated }; } }); // Reset spectator mode for next round network.isSpectator = false; ui.hideSpectatorMode(); ui.showGameOver(winner, scores); } function gameLoop(currentTime) { if (!ui.screens.game.classList.contains('active')) { return; } const deltaTime = currentTime - lastTime; lastTime = currentTime; // Update local game if (localGame) { localGame.update(deltaTime); } // Render all players const allStates = network.getAllGameStates(); Object.values(allStates).forEach(state => { renderer.renderPlayer(state.playerId, state); }); // Set active player highlight (or null for spectators to show all boards equally) renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId); requestAnimationFrame(gameLoop); }