From a65966657141d1411b141ed1b5852f3026e6208e Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Tue, 25 Mar 2025 19:11:12 -0400 Subject: [PATCH] Fix hard drop issues and improve controller responsiveness - Complete animation state reset for hard drops - Increase controller polling rate from 100ms to 16ms - Add button holding support for continuous movement --- script.js | 388 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 283 insertions(+), 105 deletions(-) diff --git a/script.js b/script.js index 2c53eb2..4078770 100644 --- a/script.js +++ b/script.js @@ -86,8 +86,19 @@ let controllerMapping = { pause: [9, 'start'] // Start button }; let lastControllerState = {}; -let controllerPollingRate = 100; // ms +let controllerPollingRate = 16; // Increased polling rate (from 100ms to 16ms for ~60fps) let controllerInterval; +let buttonHoldDelays = { + left: 150, // Initial delay before repeating + right: 150, // Initial delay before repeating + down: 50 // Fast repeat for down +}; +let buttonRepeatRates = { + left: 100, // Repeat rate after initial delay + right: 100, // Repeat rate after initial delay + down: 50 // Fast repeat rate for down +}; +let buttonHoldTimers = {}; // Fireworks array let fireworks = []; @@ -576,78 +587,67 @@ class Piece { // Rotate with 3D animation rotate(direction) { - // Direction can be 'right' or 'left' - if (gameOver || paused) return; + // Skip if already in an animation + if (this.rotationTransition || this.showCompletionEffect) { + return; + } - // Square piece (O) doesn't rotate + // Reset animation state + this.resetAnimationState(); + + // Square pieces (O) don't need to rotate if (this.tetrominoType === 'O') { return; } + // If animations are disabled, do simple rotation + if (!enableSpinAnimations || !enable3DEffects) { + // Use standard rotation without animation + let nextPattern = (direction === 'right') ? + (this.tetrominoN + 1) % this.tetromino.length : + (this.tetrominoN + this.tetromino.length - 1) % this.tetromino.length; + + // Try to rotate and apply kicks if needed + const kickResult = this.tryRotateWithKicks(nextPattern); + + if (kickResult.success) { + // Apply rotation + this.x += kickResult.kick; + this.tetrominoN = nextPattern; + this.activeTetromino = this.tetromino[this.tetrominoN]; + this.shadowTetromino = this.activeTetromino; + + // Update shadow + this.calculateShadowY(); + + // Play sound + playPieceSound('rotate'); + } + + // Draw the piece + this.draw(); + return; + } + + // Find the next pattern based on rotation direction + let nextPattern = (direction === 'right') ? + (this.tetrominoN + 1) % this.tetromino.length : + (this.tetrominoN + this.tetromino.length - 1) % this.tetromino.length; + + // Try to rotate with kicks + const kickResult = this.tryRotateWithKicks(nextPattern); + + if (!kickResult.success) { + return; // Rotation failed + } + + // We found a valid kick - save for animation completion + const validKick = kickResult.kick; + // Clear previous position this.undraw(); clearPreviousPiecePosition(); - // For I tetromino, rotation pattern is different (cycles through 0-1) - // For other tetrominoes, it cycles through 0-1-2-3 - let nextPattern; - - if (direction === 'right') { - // Rotate clockwise - if (this.tetrominoType === 'I') { - nextPattern = (this.tetrominoN + 1) % 2; - } else { - nextPattern = (this.tetrominoN + 1) % 4; - } - } else if (direction === 'left') { - // Rotate counter-clockwise - if (this.tetrominoType === 'I') { - nextPattern = (this.tetrominoN - 1 + 2) % 2; - } else { - nextPattern = (this.tetrominoN - 1 + 4) % 4; - } - } - - // Wall kick testing for rotation - // Try the standard position first, then the kickTable positions - const kicks = this.getKicks(this.tetrominoN, nextPattern); - - // Try each kick position until we find one that works - let validKick = null; - - for (let i = 0; i < kicks.length; i++) { - const [kickX, kickY] = kicks[i]; - - if (!this.collision(kickX, kickY, this.tetromino[nextPattern])) { - validKick = kickX; - // If a successful position is found, break the loop - break; - } - } - - // If no valid kick position found, keep the original position - if (validKick === null) { - this.draw(); - return; - } - - // If animations disabled, apply rotation immediately - if (!enableSpinAnimations) { - this.x += validKick; - this.tetrominoN = nextPattern; - this.activeTetromino = this.tetromino[this.tetrominoN]; - this.shadowTetromino = this.activeTetromino; - - // Play rotation sound - playPieceSound('rotate'); - - // Update shadow position - this.calculateShadowY(); - - this.draw(); - return; - } - // Store for animation completion this.targetPattern = nextPattern; this.targetKick = validKick; @@ -675,20 +675,14 @@ class Piece { return; } - // Cancel any ongoing rotations or animations - this.rotationTransition = false; - this.showCompletionEffect = false; - this.rotationAngleX = 0; - this.rotationAngleY = 0; - this.rotationAngleZ = 0; + // First, completely reset all animation state variables + // to prevent any lingering effects from rotations + this.resetAnimationState(); - // Clear rotation state completely - this.rotationDirection = null; - this.targetTetromino = null; - this.targetPattern = undefined; - this.targetKick = undefined; - this.originalTetromino = null; - this.rotationProgress = 0; + // Save current state for proper undraw + const currentTetromino = this.activeTetromino; + const currentX = this.x; + const currentY = this.y; // Clear previous position this.undraw(); @@ -699,10 +693,10 @@ class Piece { this.y++; } - // Lock the piece + // Lock the piece at its final position this.lock(); - // Replace with getNextPiece function call instead of direct assignment + // Get the next piece getNextPiece(); // Calculate shadow for new piece @@ -715,6 +709,23 @@ class Piece { playPieceSound('hardDrop'); } + // Reset all animation state + resetAnimationState() { + this.rotationTransition = false; + this.showCompletionEffect = false; + this.rotationAngleX = 0; + this.rotationAngleY = 0; + this.rotationAngleZ = 0; + this.rotationDirection = null; + this.rotationProgress = 0; + this.rotationEasing = false; + this.completionEffectProgress = 0; + this.targetTetromino = null; + this.targetPattern = undefined; + this.targetKick = undefined; + this.originalTetromino = null; + } + // 3D horizontal rotation effect (around Y axis) rotate3DY() { // Square pieces (O) shouldn't rotate @@ -722,6 +733,14 @@ class Piece { return; } + // Skip if already in an animation + if (this.rotationTransition || this.showCompletionEffect) { + return; + } + + // Reset animation state + this.resetAnimationState(); + // Clear previous position this.undraw(); clearPreviousPiecePosition(); @@ -783,6 +802,14 @@ class Piece { return; } + // Skip if already in an animation + if (this.rotationTransition || this.showCompletionEffect) { + return; + } + + // Reset animation state + this.resetAnimationState(); + // Clear previous position this.undraw(); clearPreviousPiecePosition(); @@ -1316,6 +1343,24 @@ class Piece { return kickTable[key] || [[0, 0]]; } } + + // Try rotation with wall kicks + tryRotateWithKicks(nextPattern) { + // Get kicks for this rotation + const kicks = this.getKicks(this.tetrominoN, nextPattern); + + // Try each kick position until we find one that works + for (let i = 0; i < kicks.length; i++) { + const [kickX, kickY] = kicks[i]; + + if (!this.collision(kickX, kickY, this.tetromino[nextPattern])) { + return { success: true, kick: kickX }; + } + } + + // No valid kick position found + return { success: false, kick: 0 }; + } } @@ -1765,11 +1810,50 @@ function dropPiece() { // Show game over modal function showGameOver() { + // Stop game interval clearInterval(gameInterval); + + // Clear all controller button hold timers + clearAllButtonHoldTimers(); + + // Update final score finalScoreElement.textContent = score; + + // Show modal gameOverModal.classList.add('active'); } +// Clear all button hold timers +function clearAllButtonHoldTimers() { + ['left', 'right', 'down'].forEach(action => { + if (buttonHoldTimers[action]) { + clearInterval(buttonHoldTimers[action]); + buttonHoldTimers[action] = null; + } + }); +} + +// Toggle pause +function togglePause() { + if (gameOver) return; + + paused = !paused; + + if (paused) { + clearInterval(gameInterval); + // Clear all controller button hold timers + clearAllButtonHoldTimers(); + pauseBtn.textContent = 'Resume'; + // Show pause message + showMessage('PAUSED', 'Press P to Resume'); + } else { + gameInterval = setInterval(dropPiece, Math.max(100, 1000 - (level * 100))); + pauseBtn.textContent = 'Pause'; + // Hide pause message + hideMessage(); + } +} + // Reset the game function resetGame() { // Reset game variables @@ -1805,19 +1889,6 @@ function resetGame() { gameInterval = setInterval(dropPiece, 1000); } -// Toggle pause -function togglePause() { - paused = !paused; - - if (paused) { - clearInterval(gameInterval); - pauseBtn.textContent = "Resume"; - } else { - gameInterval = setInterval(dropPiece, Math.max(100, 1000 - (level * 100))); - pauseBtn.textContent = "Pause"; - } -} - // Toggle options modal function toggleOptionsModal() { if (optionsModal.classList.contains('active')) { @@ -2074,7 +2145,7 @@ function initControllerSupport() { gamepadConnected = true; controllers[e.gamepad.index] = e.gamepad; - // Start polling for controller input if not already + // Start polling for controller input at faster rate if (!controllerInterval) { controllerInterval = setInterval(pollControllers, controllerPollingRate); } @@ -2092,14 +2163,18 @@ function initControllerSupport() { gamepadConnected = false; clearInterval(controllerInterval); controllerInterval = null; + + // Clear any ongoing button holds + clearAllButtonHoldTimers(); } // Show controller disconnected message showControllerMessage('Controller disconnected'); }); - // Initial scan + // Initial scan and setup if (Object.keys(controllers).length > 0) { + gamepadConnected = true; controllerInterval = setInterval(pollControllers, controllerPollingRate); } } @@ -2132,10 +2207,13 @@ function pollControllers() { if (!lastControllerState[controller.index]) { lastControllerState[controller.index] = { buttons: Array(controller.buttons.length).fill(false), - axes: Array(controller.axes.length).fill(0) + axes: Array(controller.axes.length).fill(0), + holdStartTimes: {} }; } + const now = Date.now(); + // Check buttons for (let action in controllerMapping) { const buttonIndex = controllerMapping[action][0]; @@ -2144,12 +2222,42 @@ function pollControllers() { if (buttonIndex >= 0 && buttonIndex < controller.buttons.length) { const button = controller.buttons[buttonIndex]; const pressed = button.pressed || button.value > 0.5; + const wasPressed = lastControllerState[controller.index].buttons[buttonIndex]; - // Check if this is a new press (wasn't pressed last time) - if (pressed && !lastControllerState[controller.index].buttons[buttonIndex]) { - // Handle controller action + // New button press (wasn't pressed last time) + if (pressed && !wasPressed) { + // Start tracking hold time for repeatable actions + if (['left', 'right', 'down'].includes(action)) { + lastControllerState[controller.index].holdStartTimes[action] = now; + // Clear any existing timers + if (buttonHoldTimers[action]) { + clearInterval(buttonHoldTimers[action]); + } + + // Schedule repeating actions + buttonHoldTimers[action] = setInterval(() => { + if (gamepadConnected && !gameOver && !paused) { + handleControllerAction(action); + } else { + // Stop repeating if game state changes + clearInterval(buttonHoldTimers[action]); + buttonHoldTimers[action] = null; + } + }, buttonRepeatRates[action]); + } + + // Immediate action on first press handleControllerAction(action); } + // Button released + else if (!pressed && wasPressed) { + // Stop repeating on release + if (['left', 'right', 'down'].includes(action) && buttonHoldTimers[action]) { + clearInterval(buttonHoldTimers[action]); + buttonHoldTimers[action] = null; + delete lastControllerState[controller.index].holdStartTimes[action]; + } + } // Update state lastControllerState[controller.index].buttons[buttonIndex] = pressed; @@ -2157,17 +2265,87 @@ function pollControllers() { } // Handle analog stick for movement + const deadzone = 0.5; + // Left stick horizontal - if (controller.axes[0] < -0.5 && lastControllerState[controller.index].axes[0] >= -0.5) { - handleControllerAction('left'); + if (controller.axes[0] < -deadzone) { + if (lastControllerState[controller.index].axes[0] >= -deadzone) { + // Initial movement + handleControllerAction('left'); + + // Setup holding behavior + lastControllerState[controller.index].holdStartTimes['left'] = now; + if (buttonHoldTimers['left']) clearInterval(buttonHoldTimers['left']); + + buttonHoldTimers['left'] = setInterval(() => { + if (gamepadConnected && !gameOver && !paused) { + handleControllerAction('left'); + } else { + clearInterval(buttonHoldTimers['left']); + buttonHoldTimers['left'] = null; + } + }, buttonRepeatRates['left']); + } } - else if (controller.axes[0] > 0.5 && lastControllerState[controller.index].axes[0] <= 0.5) { - handleControllerAction('right'); + else if (controller.axes[0] > deadzone) { + if (lastControllerState[controller.index].axes[0] <= deadzone) { + // Initial movement + handleControllerAction('right'); + + // Setup holding behavior + lastControllerState[controller.index].holdStartTimes['right'] = now; + if (buttonHoldTimers['right']) clearInterval(buttonHoldTimers['right']); + + buttonHoldTimers['right'] = setInterval(() => { + if (gamepadConnected && !gameOver && !paused) { + handleControllerAction('right'); + } else { + clearInterval(buttonHoldTimers['right']); + buttonHoldTimers['right'] = null; + } + }, buttonRepeatRates['right']); + } + } + else { + // Stick returned to center, clear horizontal timers + if (Math.abs(lastControllerState[controller.index].axes[0]) > deadzone) { + ['left', 'right'].forEach(action => { + if (buttonHoldTimers[action]) { + clearInterval(buttonHoldTimers[action]); + buttonHoldTimers[action] = null; + } + }); + } } - // Left stick vertical - if (controller.axes[1] > 0.5 && lastControllerState[controller.index].axes[1] <= 0.5) { - handleControllerAction('down'); + // Left stick vertical - down only + if (controller.axes[1] > deadzone) { + if (lastControllerState[controller.index].axes[1] <= deadzone) { + // Initial movement + handleControllerAction('down'); + + // Setup holding behavior + lastControllerState[controller.index].holdStartTimes['down'] = now; + if (buttonHoldTimers['down']) clearInterval(buttonHoldTimers['down']); + + buttonHoldTimers['down'] = setInterval(() => { + if (gamepadConnected && !gameOver && !paused) { + handleControllerAction('down'); + } else { + clearInterval(buttonHoldTimers['down']); + buttonHoldTimers['down'] = null; + } + }, buttonRepeatRates['down']); // Faster repeat for down + } + } + else { + // Stick returned to center, clear down timer + if (lastControllerState[controller.index].axes[1] > deadzone) { + if (buttonHoldTimers['down']) { + clearInterval(buttonHoldTimers['down']); + buttonHoldTimers['down'] = null; + } + } } // Update axes state