diff --git a/index.html b/index.html index 4648ed2..ea7f4d5 100644 --- a/index.html +++ b/index.html @@ -80,32 +80,34 @@ diff --git a/script.js b/script.js index 3b50e12..f51d7f3 100644 --- a/script.js +++ b/script.js @@ -12,6 +12,8 @@ const PREVIEW_BLOCK_SIZE = 25; // Mobile detection const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); let touchControls = false; +// Mobile performance mode to reduce effects on mobile +let mobilePerformanceMode = isMobile; // 7-bag randomization variables let pieceBag = []; @@ -67,6 +69,7 @@ let enable3DEffects = true; let enableSpinAnimations = true; let animationSpeed = 0.05; let forceMobileControls = false; +let reduceEffectsOnMobile = true; // New option to reduce effects on mobile // Controller variables let gamepadConnected = false; @@ -317,26 +320,19 @@ class Firework { this.x = x; this.y = y; this.particles = []; - this.particleCount = 50; - this.gravity = 0.2; this.isDone = false; - this.colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF']; // Create particles - for (let i = 0; i < this.particleCount; i++) { - const angle = Math.random() * Math.PI * 2; - const speed = Math.random() * 3 + 2; - const size = Math.random() * 3 + 1; - const color = this.colors[Math.floor(Math.random() * this.colors.length)]; - + const particleCount = mobilePerformanceMode ? 15 : 30; + + for (let i = 0; i < particleCount; i++) { this.particles.push({ x: this.x, y: this.y, - vx: Math.cos(angle) * speed, - vy: Math.sin(angle) * speed, - size: size, - color: color, - alpha: 1 + vx: (Math.random() - 0.5) * 8, + vy: (Math.random() - 0.5) * 8, + alpha: 1, + color: `hsl(${Math.random() * 360}, 100%, 50%)` }); } } @@ -352,11 +348,12 @@ class Firework { p.y += p.vy; // Apply gravity - p.vy += this.gravity; + p.vy += 0.1; - // Reduce alpha (fade out) - p.alpha -= 0.01; + // Fade out + p.alpha -= 0.02; + // Check if any particles are still visible if (p.alpha > 0) { allDone = false; } @@ -371,21 +368,22 @@ class Firework { if (p.alpha <= 0) continue; + ctx.save(); ctx.globalAlpha = p.alpha; ctx.fillStyle = p.color; - ctx.beginPath(); - ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); - ctx.closePath(); - ctx.fill(); + + // Draw smaller particles on mobile for better performance + const size = mobilePerformanceMode ? 3 : 5; + ctx.fillRect(p.x, p.y, size, size); + + ctx.restore(); } - - ctx.globalAlpha = 1; // Reset alpha } } // The Piece class class Piece { - constructor(tetromino, tetrominoN, color) { + constructor(tetromino, tetrominoN, color, tetrominoType) { this.tetromino = tetromino; this.color = color; @@ -393,6 +391,9 @@ class Piece { this.activeTetromino = this.tetromino[this.tetrominoN]; this.shadowTetromino = this.activeTetromino; // For shadow calculation + // Store piece type for SRS rotation kicks + this.tetrominoType = tetrominoType; + // Starting position this.x = 3; this.y = -2; @@ -575,63 +576,86 @@ class Piece { // Rotate with 3D animation rotate(direction) { + // Direction can be 'right' or 'left' + if (gameOver || paused) return; + // Clear previous position this.undraw(); clearPreviousPiecePosition(); - // Determine next pattern + // 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') { - nextPattern = (this.tetrominoN + 1) % this.tetromino.length; - } else { - nextPattern = (this.tetrominoN - 1 + this.tetromino.length) % this.tetromino.length; - } - - // Check for wall kicks - let kick = 0; - if (this.collision(0, 0, this.tetromino[nextPattern])) { - if (this.x > COLS / 2) { - // Right wall collision - kick = -1; + // Rotate clockwise + if (this.tetrominoType === 'I') { + nextPattern = (this.tetrominoN + 1) % 2; } else { - // Left wall collision - kick = 1; + 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; } } - // If animations are disabled, apply change immediately - if (!enableSpinAnimations) { - // Apply pattern if not colliding - if (!this.collision(kick, 0, this.tetromino[nextPattern])) { - this.x += kick; - this.tetrominoN = nextPattern; - this.activeTetromino = this.tetromino[this.tetrominoN]; - this.shadowTetromino = this.activeTetromino; - - // Update shadow position - this.calculateShadowY(); - - // Play rotation sound - playPieceSound('rotate'); - - // Draw the piece - this.draw(); + // 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; } - // For animated version, start rotation transition + // 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; + + // Start rotation transition this.rotationTransition = true; this.rotationDirection = direction === 'right' ? 'rotateRight' : 'rotateLeft'; this.rotationProgress = 0; + this.rotationEasing = true; - // Store current tetromino + // Store original tetromino for animation this.originalTetromino = this.activeTetromino; - // Set target tetrominoN and position - this.targetPattern = nextPattern; - this.targetKick = kick; - // Play rotation sound playPieceSound('rotate'); @@ -813,11 +837,10 @@ class Piece { lock() { for (let r = 0; r < this.activeTetromino.length; r++) { for (let c = 0; c < this.activeTetromino[r].length; c++) { - if (!this.activeTetromino[r][c]) { - continue; - } + // Skip empty cells + if (!this.activeTetromino[r][c]) continue; - // Game over if piece is above the board + // Game over when piece is locked outside the board if (this.y + r < 0) { gameOver = true; break; @@ -828,41 +851,48 @@ class Piece { } } - // Remove full rows and track their positions for fireworks + // Clear full rows let linesCleared = 0; let clearedRows = []; for (let r = 0; r < ROWS; r++) { let isRowFull = true; + + // Check if the row is full for (let c = 0; c < COLS; c++) { - isRowFull = isRowFull && (board[r][c] !== EMPTY); + if (board[r][c] === EMPTY) { + isRowFull = false; + break; + } } + // If the row is full, clear it if (isRowFull) { - // Store the row index for fireworks clearedRows.push(r); + linesCleared++; - // Remove the row - for (let y = r; y > 1; y--) { + // Shift rows down + for (let y = r; y > 0; y--) { for (let c = 0; c < COLS; c++) { board[y][c] = board[y-1][c]; } } - // Top row + // Clear the top row for (let c = 0; c < COLS; c++) { board[0][c] = EMPTY; } - - linesCleared++; } } // Create fireworks for each cleared row if (clearedRows.length > 0) { + // Reduce number of fireworks on mobile for better performance + const fireworksPerRow = mobilePerformanceMode ? 1 : 3; + for (let i = 0; i < clearedRows.length; i++) { // Create multiple fireworks along the row - for (let j = 0; j < 3; j++) { + for (let j = 0; j < fireworksPerRow; j++) { const x = (Math.random() * COLS * BLOCK_SIZE) + BLOCK_SIZE/2; const y = (clearedRows[i] * BLOCK_SIZE) + BLOCK_SIZE/2; fireworks.push(new Firework(x, y)); @@ -972,41 +1002,59 @@ class Piece { animate3DRotation() { if (!this.rotationTransition && !this.showCompletionEffect) return; + // Faster animation on mobile for better performance + const animationStep = mobilePerformanceMode ? animationSpeed * 1.5 : animationSpeed; + if (this.rotationTransition) { - // Increment progress - use the global animation speed - this.rotationProgress += animationSpeed; + // Update rotation progress + this.rotationProgress += animationStep; - // When rotation is complete if (this.rotationProgress >= 1) { - this.rotationTransition = false; - this.showCompletionEffect = enable3DEffects; // Only show completion effect if 3D effects are enabled - this.completionEffectProgress = 0; - - // Apply the target pattern/tetromino - if (this.rotationDirection === 'horizontal' || this.rotationDirection === 'vertical') { - // Apply the mirror if it doesn't cause collision - if (!this.collision(0, 0, this.targetTetromino)) { - this.activeTetromino = this.targetTetromino; - this.shadowTetromino = this.activeTetromino; - } - } else if (this.rotationDirection === 'rotateLeft' || this.rotationDirection === 'rotateRight') { - // Apply the rotation if it doesn't cause collision - if (!this.collision(this.targetKick, 0, this.tetromino[this.targetPattern])) { - this.x += this.targetKick; - this.tetrominoN = this.targetPattern; - this.activeTetromino = this.tetromino[this.tetrominoN]; - this.shadowTetromino = this.activeTetromino; + // Animation complete, apply the final result + // Check if the target tetromino would collide with anything + if (!this.targetTetromino) { + // Handle rotation without target tetromino (for standard rotations) + if (this.targetPattern !== undefined && this.targetKick !== undefined) { + // Apply the rotation if it doesn't cause collision + if (!this.collision(this.targetKick, 0, this.tetromino[this.targetPattern])) { + this.x += this.targetKick; + this.tetrominoN = this.targetPattern; + this.activeTetromino = this.tetromino[this.tetrominoN]; + this.shadowTetromino = this.activeTetromino; + } } + } else if (this.collision(0, 0, this.targetTetromino)) { + // If collision, revert to original position for mirror operations + this.activeTetromino = this.originalTetromino; + this.shadowTetromino = this.originalTetromino; + } else { + // Apply the mirror/rotation + this.activeTetromino = this.targetTetromino; + this.shadowTetromino = this.targetTetromino; } - // Update shadow position + // Reset rotation state + this.rotationTransition = false; + this.rotationProgress = 0; + + // Reset rotation angles + this.rotationAngleX = 0; + this.rotationAngleY = 0; + this.rotationAngleZ = 0; + + // Recalculate shadow position this.calculateShadowY(); - // If no completion effect, we're done - if (!this.showCompletionEffect) { - this.draw(); - return; + // Skip completion effect on mobile for performance + if (!mobilePerformanceMode && enable3DEffects) { + // Start completion effect + this.showCompletionEffect = true; + this.completionEffectProgress = 0; } + + // Draw in new position + this.draw(); + return; } // Calculate current rotation angles based on progress with easing @@ -1029,7 +1077,9 @@ class Piece { } } else if (this.showCompletionEffect) { // Handle completion effect animation - this.completionEffectProgress += 0.1; + // Use faster animation on mobile + const completionStep = mobilePerformanceMode ? 0.15 : 0.1; + this.completionEffectProgress += completionStep; if (this.completionEffectProgress >= 1) { this.showCompletionEffect = false; @@ -1043,8 +1093,12 @@ class Piece { // Redraw with current rotation this.draw(); - // Continue animation - requestAnimationFrame(() => this.animate3DRotation()); + // Continue animation - use lower frame rate on mobile + if (mobilePerformanceMode) { + setTimeout(() => this.animate3DRotation(), 16); // ~60fps + } else { + requestAnimationFrame(() => this.animate3DRotation()); + } } // New method to draw with 3D rotation effect @@ -1058,6 +1112,9 @@ class Piece { // Sort blocks by depth for proper rendering let blocks = []; + // Reduce complexity on mobile devices + const skipGradientOnMobile = mobilePerformanceMode; + for (let r = 0; r < tetromino.length; r++) { for (let c = 0; c < tetromino[r].length; c++) { if (!tetromino[r][c]) continue; @@ -1081,8 +1138,8 @@ class Piece { transX = relX * Math.cos(angle); depth = relX * Math.sin(angle); - // Add perspective effect - const perspective = 0.2; + // Add perspective effect - less intense on mobile + const perspective = mobilePerformanceMode ? 0.15 : 0.2; const perspectiveScale = 1 + depth * perspective; transX /= perspectiveScale; transY /= perspectiveScale; @@ -1096,13 +1153,13 @@ class Piece { transY = relY * Math.cos(angle); depth = relY * Math.sin(angle); - // Add perspective effect - const perspective = 0.2; + // Add perspective effect - less intense on mobile + const perspective = mobilePerformanceMode ? 0.15 : 0.2; const perspectiveScale = 1 + depth * perspective; transX /= perspectiveScale; transY /= perspectiveScale; scale /= perspectiveScale; - } else if (this.rotationDirection === 'rotateRight' || this.rotationDirection === 'rotateLeft') { + } else { // Z-axis rotation for regular tetris rotations const angle = this.rotationAngleZ; @@ -1114,8 +1171,9 @@ class Piece { scale = 1.0; // Add subtle depth effect based on rotation progress + // Reduce effect on mobile const rotationProgress = Math.abs(angle) / (Math.PI * 2); - depth = 0.3 * Math.sin(rotationProgress * Math.PI); + depth = (mobilePerformanceMode ? 0.2 : 0.3) * Math.sin(rotationProgress * Math.PI); } // Apply transformation @@ -1140,7 +1198,7 @@ class Piece { for (const block of blocks) { if (block.scale > 0) { // Apply scale to create 3D effect with depth - draw3DSquare(block.x, block.y, this.color, block.scale, block.depth); + draw3DSquare(block.x, block.y, this.color, block.scale, block.depth, skipGradientOnMobile); } } } @@ -1165,8 +1223,54 @@ class Piece { } } } + + // Get wall kicks for rotation based on tetromino type and rotation state + getKicks(currentRotation, nextRotation) { + // Special handling for I tetromino (uses different kick table) + if (this.tetrominoType === 'I') { + // Wall kick data for I tetromino (SRS) + const kickTableI = { + "0-1": [[0, 0], [-2, 0], [1, 0], [-2, -1], [1, 2]], + "1-0": [[0, 0], [2, 0], [-1, 0], [2, 1], [-1, -2]], + "1-2": [[0, 0], [-1, 0], [2, 0], [-1, 2], [2, -1]], + "2-1": [[0, 0], [1, 0], [-2, 0], [1, -2], [-2, 1]], + "2-3": [[0, 0], [2, 0], [-1, 0], [2, 1], [-1, -2]], + "3-2": [[0, 0], [-2, 0], [1, 0], [-2, -1], [1, 2]], + "3-0": [[0, 0], [1, 0], [-2, 0], [1, -2], [-2, 1]], + "0-3": [[0, 0], [-1, 0], [2, 0], [-1, 2], [2, -1]] + }; + + // Convert 0-3 rotation to 0-1 for I tetromino + const from = currentRotation % 2; + const to = nextRotation % 2; + const key = `${from}-${to}`; + + return kickTableI[key] || [[0, 0]]; + } else { + // Wall kick data for JLSTZ tetrominoes (SRS) + const kickTable = { + "0-1": [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], + "1-0": [[0, 0], [1, 0], [1, -1], [0, 2], [1, 2]], + "1-2": [[0, 0], [1, 0], [1, -1], [0, 2], [1, 2]], + "2-1": [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], + "2-3": [[0, 0], [1, 0], [1, 1], [0, -2], [1, -2]], + "3-2": [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], + "3-0": [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], + "0-3": [[0, 0], [1, 0], [1, 1], [0, -2], [1, -2]] + }; + + // Handle O tetromino (no rotation) + if (this.tetrominoType === 'O') { + return [[0, 0]]; + } + + const key = `${currentRotation}-${nextRotation}`; + return kickTable[key] || [[0, 0]]; + } + } } + // Draw a shadow square on the board function drawShadowSquare(x, y) { if (y < 0) return; // Don't draw above the board @@ -1258,105 +1362,54 @@ function drawSquare(x, y, color) { } // Draw a 3D square with rotation effect -function draw3DSquare(x, y, color, scale, depth = 0) { - if (y < 0) return; // Don't draw above the board +function draw3DSquare(x, y, color, scale, depth = 0, skipGradient = false) { + // Convert block coordinates to pixel coordinates + const pixelX = x * BLOCK_SIZE; + const pixelY = y * BLOCK_SIZE; - // Save the context state + // Calculate actual size based on scale + const size = BLOCK_SIZE * scale; + + // Calculate offset to keep block centered + const offsetX = (BLOCK_SIZE - size) / 2; + const offsetY = (BLOCK_SIZE - size) / 2; + + // Save context state ctx.save(); - // Calculate the position with scaling from center of block - const centerX = (x + 0.5) * BLOCK_SIZE; - const centerY = (y + 0.5) * BLOCK_SIZE; - const scaledSize = BLOCK_SIZE * Math.abs(scale); - const offsetX = centerX - scaledSize / 2; - const offsetY = centerY - scaledSize / 2; + // Draw the block + ctx.fillStyle = color; + ctx.fillRect(pixelX + offsetX, pixelY + offsetY, size, size); - // Create gradient for filled squares - const gradient = ctx.createLinearGradient( - offsetX, - offsetY, - offsetX + scaledSize, - offsetY + scaledSize - ); + // Apply gradient for 3D effect - skip for performance mode + if (!skipGradient) { + // Lighter side (light source from top-left) + const lightVal = Math.min(255, 150 + depth * 200); + ctx.fillStyle = `rgba(${lightVal}, ${lightVal}, ${lightVal}, 0.3)`; + ctx.beginPath(); + ctx.moveTo(pixelX + offsetX, pixelY + offsetY); + ctx.lineTo(pixelX + offsetX + size, pixelY + offsetY); + ctx.lineTo(pixelX + offsetX + size / 2, pixelY + offsetY + size / 2); + ctx.closePath(); + ctx.fill(); + + // Darker side (shadow on bottom-right) + const shadowVal = Math.max(0, 50 - depth * 100); + ctx.fillStyle = `rgba(${shadowVal}, ${shadowVal}, ${shadowVal}, 0.3)`; + ctx.beginPath(); + ctx.moveTo(pixelX + offsetX + size, pixelY + offsetY + size); + ctx.lineTo(pixelX + offsetX, pixelY + offsetY + size); + ctx.lineTo(pixelX + offsetX + size / 2, pixelY + offsetY + size / 2); + ctx.closePath(); + ctx.fill(); + } - const gradColors = GRADIENT_COLORS[color] || [color, color]; + // Draw border + ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + ctx.lineWidth = 1; + ctx.strokeRect(pixelX + offsetX, pixelY + offsetY, size, size); - // Adjust color based on depth - const depthFactor = 0.7 + 0.3 * (1 - Math.min(1, Math.abs(depth))); - const adjustColor = (color) => { - // Simple color brightening/darkening based on depth - if (color.startsWith('#')) { - // Convert hex to RGB - const r = parseInt(color.slice(1, 3), 16); - const g = parseInt(color.slice(3, 5), 16); - const b = parseInt(color.slice(5, 7), 16); - - // Adjust brightness - const newR = Math.min(255, Math.floor(r * depthFactor)); - const newG = Math.min(255, Math.floor(g * depthFactor)); - const newB = Math.min(255, Math.floor(b * depthFactor)); - - // Convert back to hex - return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; - } - return color; - }; - - const adjustedColors = [ - adjustColor(gradColors[0]), - adjustColor(gradColors[1]) - ]; - - gradient.addColorStop(0, adjustedColors[0]); - gradient.addColorStop(1, adjustedColors[1]); - - // Add glow effect - ctx.shadowColor = adjustedColors[0]; - ctx.shadowBlur = 10 * Math.abs(scale); - - // Fill with gradient - ctx.fillStyle = gradient; - ctx.fillRect(offsetX, offsetY, scaledSize, scaledSize); - - // Reset shadow for clean edges - ctx.shadowColor = 'transparent'; - ctx.shadowBlur = 0; - - // Scale line width with the block - const lineWidth = 2 * Math.abs(scale); - - // Add highlights and shadow - scaled - ctx.strokeStyle = '#fff'; - ctx.globalAlpha = depthFactor; // Make lines fade with depth - ctx.lineWidth = lineWidth; - ctx.beginPath(); - ctx.moveTo(offsetX, offsetY); - ctx.lineTo(offsetX + scaledSize, offsetY); - ctx.lineTo(offsetX + scaledSize, offsetY + scaledSize * 0.3); - ctx.moveTo(offsetX, offsetY); - ctx.lineTo(offsetX, offsetY + scaledSize); - ctx.lineTo(offsetX + scaledSize * 0.3, offsetY + scaledSize); - ctx.stroke(); - - ctx.strokeStyle = '#333'; - ctx.lineWidth = lineWidth; - ctx.beginPath(); - ctx.moveTo(offsetX + scaledSize, offsetY); - ctx.lineTo(offsetX + scaledSize, offsetY + scaledSize); - ctx.lineTo(offsetX, offsetY + scaledSize); - ctx.stroke(); - - // Add inner glow - ctx.fillStyle = `rgba(255, 255, 255, ${0.2 * depthFactor})`; - const innerPadding = 4 * Math.abs(scale); - ctx.fillRect( - offsetX + innerPadding, - offsetY + innerPadding, - scaledSize - innerPadding * 2, - scaledSize - innerPadding * 2 - ); - - // Restore the context state + // Restore context ctx.restore(); } @@ -1569,6 +1622,9 @@ function playLineClearSound(lineCount) { // Update game frame function update() { + // Limit fireworks on mobile for performance + const maxFireworks = mobilePerformanceMode ? 5 : 15; + // Update and draw fireworks for (let i = fireworks.length - 1; i >= 0; i--) { fireworks[i].update(); @@ -1578,6 +1634,11 @@ function update() { } } + // Limit fireworks count for performance on mobile + if (fireworks.length > maxFireworks) { + fireworks.splice(0, fireworks.length - maxFireworks); + } + // Request the next frame requestAnimationFrame(update); } @@ -1604,14 +1665,16 @@ function draw() { } // Draw fireworks (clipped to canvas area) - ctx.save(); - ctx.beginPath(); - ctx.rect(0, 0, canvas.width, canvas.height); - ctx.clip(); - for (let i = 0; i < fireworks.length; i++) { - fireworks[i].draw(); + if (!mobilePerformanceMode || fireworks.length < 3) { + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.clip(); + for (let i = 0; i < fireworks.length; i++) { + fireworks[i].draw(); + } + ctx.restore(); } - ctx.restore(); // Draw messages if game is paused if (paused) { @@ -1734,6 +1797,17 @@ function applyOptions() { document.body.classList.remove('mobile-mode'); } + // Apply performance optimizations for mobile + if (reduceEffectsOnMobile && isMobile) { + mobilePerformanceMode = true; + // Reduce effects automatically on mobile for better performance + if (document.getElementById('toggle-mobile-performance').checked === false) { + document.getElementById('toggle-mobile-performance').checked = true; + } + } else { + mobilePerformanceMode = document.getElementById('toggle-mobile-performance').checked; + } + // Save options saveOptions(); } @@ -1744,7 +1818,8 @@ function saveOptions() { enable3DEffects, enableSpinAnimations, animationSpeed, - forceMobileControls + forceMobileControls, + reduceEffectsOnMobile })); } @@ -1758,6 +1833,7 @@ function loadOptions() { enableSpinAnimations = options.enableSpinAnimations !== undefined ? options.enableSpinAnimations : true; animationSpeed = options.animationSpeed !== undefined ? options.animationSpeed : 0.05; forceMobileControls = options.forceMobileControls !== undefined ? options.forceMobileControls : false; + reduceEffectsOnMobile = options.reduceEffectsOnMobile !== undefined ? options.reduceEffectsOnMobile : true; // Update UI controls toggle3DEffects.checked = enable3DEffects; @@ -1767,6 +1843,10 @@ function loadOptions() { if (toggleMobileControls) { toggleMobileControls.checked = forceMobileControls; } + + if (document.getElementById('toggle-mobile-performance')) { + document.getElementById('toggle-mobile-performance').checked = reduceEffectsOnMobile; + } } } @@ -1822,6 +1902,13 @@ function init() { applyOptions(); }); + if (document.getElementById('toggle-mobile-performance')) { + document.getElementById('toggle-mobile-performance').addEventListener('change', function() { + mobilePerformanceMode = this.checked; + applyOptions(); + }); + } + if (toggleMobileControls) { toggleMobileControls.addEventListener('change', function() { applyOptions(); @@ -2293,29 +2380,6 @@ function handleTouchEnd(event) { Math.pow(touchY - lastTapY, 2) ); - const distanceFromSecondLastTap = Math.sqrt( - Math.pow(touchX - secondLastTapX, 2) + - Math.pow(touchY - secondLastTapY, 2) - ); - - // Check for triple tap - all three taps must be close in position and time - if (touchEndTime - secondLastTapTime < TRIPLE_TAP_THRESHOLD && - distanceFromLastTap < TAP_DISTANCE_THRESHOLD && - distanceFromSecondLastTap < TAP_DISTANCE_THRESHOLD) { - - // Execute 3D rotation (randomly choose horizontal or vertical) - if (Math.random() > 0.5) { - p.rotate3DX(); - } else { - p.rotate3DY(); - } - - // Reset tap tracking after triple tap - secondLastTapTime = 0; - lastTapTime = 0; - return; - } - // Check for double tap (for hard drop) const timeBetweenTaps = touchEndTime - lastTapTime; @@ -2323,13 +2387,8 @@ function handleTouchEnd(event) { // This is a double-tap, do hard drop p.hardDrop(); - // Store for potential triple tap - secondLastTapTime = lastTapTime; - secondLastTapX = lastTapX; - secondLastTapY = lastTapY; - lastTapTime = touchEndTime; - lastTapX = touchX; - lastTapY = touchY; + // Reset tap tracking + lastTapTime = 0; // Debug console.log("Double tap detected - hard drop"); @@ -2339,15 +2398,7 @@ function handleTouchEnd(event) { // Single tap - rotates piece p.rotate('right'); - // Update tracking for potential double/triple tap - if (lastTapTime > 0) { - // Store previous tap data - secondLastTapTime = lastTapTime; - secondLastTapX = lastTapX; - secondLastTapY = lastTapY; - } - - // Set current tap as the last tap + // Update tracking for potential double tap lastTapTime = touchEndTime; lastTapX = touchX; lastTapY = touchY; @@ -2359,7 +2410,29 @@ function handleTouchEnd(event) { // Create touch instructions overlay function createTouchControlButtons() { - // Do not create buttons - using gesture-based controls only + // Create 3D rotation buttons + const rotateButtons = document.createElement('div'); + rotateButtons.className = 'rotate-buttons'; + rotateButtons.innerHTML = ` + + + `; + document.body.appendChild(rotateButtons); + + // Add event listeners + document.getElementById('rotate3d-x').addEventListener('click', function(e) { + e.preventDefault(); + if (!gameOver && !paused) { + p.rotate3DX(); + } + }); + + document.getElementById('rotate3d-y').addEventListener('click', function(e) { + e.preventDefault(); + if (!gameOver && !paused) { + p.rotate3DY(); + } + }); // Create touch instructions overlay const touchInstructions = document.createElement('div'); @@ -2370,7 +2443,7 @@ function createTouchControlButtons() {

Swipe down: Soft drop

Tap anywhere: Rotate right

Double-tap: Hard drop

-

Triple-tap: 3D rotate

+

3D Flip Buttons: Rotate in 3D

`; document.body.appendChild(touchInstructions); @@ -2408,6 +2481,31 @@ function createTouchControlButtons() { if (scoreContainer) { scoreContainer.appendChild(instructionsBtn); } + + // Add performance mode toggle + const perfToggle = document.createElement('input'); + perfToggle.type = 'checkbox'; + perfToggle.id = 'toggle-mobile-performance'; + perfToggle.checked = mobilePerformanceMode; + + const perfLabel = document.createElement('label'); + perfLabel.htmlFor = 'toggle-mobile-performance'; + perfLabel.textContent = 'Perf Mode'; + perfLabel.className = 'perf-label'; + + const perfContainer = document.createElement('div'); + perfContainer.className = 'perf-toggle'; + perfContainer.appendChild(perfToggle); + perfContainer.appendChild(perfLabel); + + if (scoreContainer) { + scoreContainer.appendChild(perfContainer); + } + + // Add event listener + perfToggle.addEventListener('change', function() { + mobilePerformanceMode = this.checked; + }); } // Set active piece to next piece and create new next piece @@ -2496,10 +2594,23 @@ function getNextPieceFromBag() { const tetromino = PIECES[pieceIndex]; const color = COLORS[pieceIndex]; + // Get the tetromino type based on index + let tetrominoType; + switch(pieceIndex) { + case 0: tetrominoType = 'I'; break; + case 1: tetrominoType = 'J'; break; + case 2: tetrominoType = 'L'; break; + case 3: tetrominoType = 'O'; break; + case 4: tetrominoType = 'S'; break; + case 5: tetrominoType = 'T'; break; + case 6: tetrominoType = 'Z'; break; + default: tetrominoType = 'X'; + } + // Random rotation/orientation const randomIndex = Math.floor(Math.random() * tetromino.length); - return new Piece(tetromino, randomIndex, color); + return new Piece(tetromino, randomIndex, color, tetrominoType); } // Start the game diff --git a/style.css b/style.css index d6c8c1b..e52a7de 100644 --- a/style.css +++ b/style.css @@ -249,7 +249,7 @@ canvas { left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.8); z-index: 100; justify-content: center; align-items: center; @@ -260,25 +260,23 @@ canvas { } .modal-content { - background: rgba(0, 0, 0, 0.9); - padding: 30px; + background: rgba(10, 10, 20, 0.85); + border: 2px solid rgba(0, 255, 255, 0.6); border-radius: 10px; + color: white; + padding: 20px; + width: 90%; + max-width: 500px; + box-shadow: 0 0 30px rgba(0, 255, 255, 0.5); text-align: center; - border: 2px solid #444; - box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); - animation: modal-appear 0.5s; } -@keyframes modal-appear { - from {transform: scale(0.8); opacity: 0;} - to {transform: scale(1); opacity: 1;} -} - -.modal-content h2 { - font-size: 24px; - margin-bottom: 20px; - color: #ff0066; - text-shadow: 0 0 10px #ff0066; +.modal h2 { + margin-top: 0; + color: #00ffff; + text-shadow: 0 0 10px rgba(0, 255, 255, 0.7); + text-transform: uppercase; + letter-spacing: 2px; } .modal-content p { @@ -387,17 +385,32 @@ canvas#tetris { /* Options menu styles */ .option-row { display: flex; - justify-content: space-between; align-items: center; - margin: 20px 0; - padding: 10px; - background: rgba(51, 51, 51, 0.8); - border-radius: 5px; + margin: 15px 0; + position: relative; } .option-row label { - font-size: 14px; - color: #fff; + margin-right: 10px; + min-width: 180px; +} + +.tooltip { + font-size: 12px; + color: #aaa; + font-style: italic; + margin-left: 10px; + max-width: 200px; +} + +.mobile-only { + display: none; +} + +@media (max-width: 768px) { + .mobile-only { + display: flex; + } } /* Toggle switch styles */ @@ -497,6 +510,86 @@ input[type=range]::-moz-range-thumb { touch-action: none; } +/* 3D Rotation buttons */ +.rotate-buttons { + position: fixed; + bottom: 20px; + right: 20px; + display: flex; + flex-direction: column; + gap: 10px; + z-index: 100; +} + +.rotate-btn { + background: linear-gradient(45deg, #007bff, #00ddff); + color: white; + border: none; + padding: 12px 15px; + border-radius: 50px; + font-size: 14px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + transition: all 0.2s ease; + font-weight: bold; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.rotate-btn:active { + transform: scale(0.95); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + background: linear-gradient(45deg, #0062cc, #00b3ff); +} + +/* Performance toggle */ +.perf-toggle { + display: flex; + align-items: center; + margin-top: 5px; + background: rgba(0, 0, 0, 0.3); + padding: 5px; + border-radius: 4px; + font-size: 11px; +} + +.perf-label { + margin-left: 5px; + color: #00ffff; + cursor: pointer; +} + +#toggle-mobile-performance { + cursor: pointer; +} + +/* Mobile mode adjustments for rotation buttons */ +.mobile-mode .rotate-buttons { + bottom: 15px; + right: 15px; + gap: 8px; +} + +.mobile-mode .rotate-btn { + padding: 10px 12px; + font-size: 12px; +} + +/* Mobile landscape orientation */ +@media (orientation: landscape) { + .mobile-mode .rotate-buttons { + flex-direction: row; + bottom: 10px; + right: 10px; + } +} + +/* Smaller devices */ +@media (max-width: 400px) { + .mobile-mode .rotate-btn { + padding: 8px 10px; + font-size: 11px; + } +} + .mobile-mode .game-container { flex-direction: row; align-items: flex-start;