// Main Application - Ties everything together let localGame = null; let renderer = null; let lastTime = 0; // Keyboard state for continuous movement (DAS - Delayed Auto Shift) const keyState = { ArrowLeft: false, ArrowRight: false, ArrowDown: false, }; // Track key press timing for tap vs hold detection let keyPressStartTime = null; let wasAutoMoved = { ArrowLeft: false, ArrowRight: false, ArrowDown: false, }; let dasCounter = 0; const DAS_DELAY = 75; // ms before auto-repeat starts (reduced from 100 for faster response) const ARR_SPEED = 25; // ms between auto-repeat moves (reduced from 30 for faster movement) // 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'); // Play lobby music when first entering lobby if (typeof audio !== 'undefined') { audio.playLobby(); } } // Reset lobby state when returning from game over (player is null indicates game over reset) if (!player) { ui.resetLobbyState(); // Resume lobby music after game ends if (typeof audio !== 'undefined') { audio.playLobby(); } } }); // 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'; // Start game music - will play continuously until game ends if (typeof audio !== 'undefined') { audio.playGame(); } // 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); } // Game over is handled by the server via 'game-over' event // Don't check locally to avoid premature game end }); // 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(); // Start game music for spectators too if (typeof audio !== 'undefined') { audio.playGame(); } // 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) { // Track held keys for movement if (keyState.hasOwnProperty(e.key)) { keyState[e.key] = true; keyPressStartTime = performance.now(); // Track when key was pressed wasAutoMoved[e.key] = false; // Reset auto-move flag for tap detection dasCounter = 0; // Reset DAS counter when key pressed e.preventDefault(); return; } // One-time actions switch (e.key) { case 'ArrowUp': network.sendRotate(); e.preventDefault(); break; case ' ': network.sendHardDrop(); e.preventDefault(); break; case 'Shift': network.sendHold(); e.preventDefault(); break; case 'a': case 'A': network.sendZoneActivate(); e.preventDefault(); break; } } }); document.addEventListener('keyup', (e) => { if (keyState.hasOwnProperty(e.key)) { keyState[e.key] = false; dasCounter = 0; // Tap detection: if key was held for less than DAS_DELAY and not auto-moved, // send a single move for tap-based horizontal movement if (keyPressStartTime) { const pressDuration = performance.now() - keyPressStartTime; if (pressDuration < DAS_DELAY && !wasAutoMoved[e.key]) { // It was a tap - send single move if (e.key === 'ArrowLeft') { network.sendMove('left'); } else if (e.key === 'ArrowRight') { network.sendMove('right'); } } } keyPressStartTime = null; } }); // 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 btnZone = document.getElementById('btn-zone'); 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())); btnZone.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendZoneActivate())); // 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()); btnZone.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 }; } }); // Game music will continue during game over screen, then transition back to lobby music via resetLobbyState // 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; // Handle DAS (Delayed Auto Shift) for continuous movement if (!network.isSpectator && (keyState.ArrowLeft || keyState.ArrowRight || keyState.ArrowDown)) { dasCounter += deltaTime; // Send initial move immediately, then auto-repeat after DAS_DELAY if (dasCounter >= DAS_DELAY) { const autoRepeatCounter = (dasCounter - DAS_DELAY) % ARR_SPEED; if (autoRepeatCounter < (deltaTime || 0)) { if (keyState.ArrowLeft) { network.sendMove('left'); wasAutoMoved.ArrowLeft = true; // Mark as auto-moved so keyup doesn't send another } if (keyState.ArrowRight) { network.sendMove('right'); wasAutoMoved.ArrowRight = true; } if (keyState.ArrowDown) { network.sendDrop(); wasAutoMoved.ArrowDown = true; } } } } // 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); }); // Update Zone button state for current player const btnZone = document.getElementById('btn-zone'); const localState = allStates[network.currentPlayerId]; if (btnZone && localState) { btnZone.disabled = localState.zoneMeter < 100 || localState.zoneActive; } // Set active player highlight (or null for spectators to show all boards equally) renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId); requestAnimationFrame(gameLoop); }