mirror of
https://github.com/cmclark00/tetris-3d.git
synced 2025-05-17 15:15:20 +01:00
Add performance optimizations for mobile devices
This commit is contained in:
parent
324c977b3b
commit
2b7c5175a0
1 changed files with 162 additions and 627 deletions
789
script.js
789
script.js
|
@ -1,3 +1,17 @@
|
|||
// Performance optimization variables
|
||||
let lastTimestamp = 0;
|
||||
const FPS_LIMIT = 60; // Default FPS limit
|
||||
const MOBILE_FPS_LIMIT = 30; // Lower FPS on mobile
|
||||
const FRAME_MIN_TIME = (1000 / FPS_LIMIT);
|
||||
let isReducedEffects = false; // For mobile performance
|
||||
let maxFireworks = 30; // Default limit
|
||||
let maxParticlesPerFirework = 30; // Default limit
|
||||
|
||||
// Add performance monitoring
|
||||
let frameCounter = 0;
|
||||
let lastFpsUpdate = 0;
|
||||
let currentFps = 0;
|
||||
|
||||
// Get canvas and context
|
||||
const canvas = document.getElementById('tetris');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
@ -311,7 +325,19 @@ for (let i = 0; i < PIECES.length; i++) {
|
|||
PIECE_3D_ORIENTATIONS[i].push(verticalRotation);
|
||||
}
|
||||
|
||||
// Firework class for visual effects
|
||||
// Add this to the loadOptions function
|
||||
function loadOptions() {
|
||||
// ... existing code ...
|
||||
|
||||
// Detect if we're on mobile and reduce effects automatically
|
||||
if (isMobile) {
|
||||
maxFireworks = 10;
|
||||
maxParticlesPerFirework = 15;
|
||||
isReducedEffects = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Modify the Firework constructor to use the max particles limit
|
||||
class Firework {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
|
@ -322,8 +348,13 @@ class Firework {
|
|||
this.isDone = false;
|
||||
this.colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'];
|
||||
|
||||
// Create particles
|
||||
for (let i = 0; i < this.particleCount; i++) {
|
||||
// Limit particles based on device capability
|
||||
const particleCount = isReducedEffects ?
|
||||
Math.floor(Math.random() * 10) + 5 :
|
||||
Math.floor(Math.random() * maxParticlesPerFirework) + 10;
|
||||
|
||||
// Create particles with limit
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = Math.random() * 3 + 2;
|
||||
const size = Math.random() * 3 + 1;
|
||||
|
@ -1569,31 +1600,64 @@ function playLineClearSound(lineCount) {
|
|||
|
||||
// Update game frame
|
||||
function update() {
|
||||
// Update and draw fireworks
|
||||
for (let i = fireworks.length - 1; i >= 0; i--) {
|
||||
fireworks[i].update();
|
||||
// Remove old fireworks
|
||||
fireworks = fireworks.filter(fw => !fw.done);
|
||||
|
||||
// Only create new fireworks if we're below the limit and not on low-end devices
|
||||
if (!gameOver && !paused) {
|
||||
const maxAllowed = isReducedEffects ? maxFireworks/2 : maxFireworks;
|
||||
const creationProbability = isReducedEffects ? 0.005 : 0.02; // Lower probability on mobile
|
||||
|
||||
if (fireworks[i].isDone) {
|
||||
fireworks.splice(i, 1);
|
||||
if (fireworks.length < maxAllowed && Math.random() < creationProbability) {
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
fireworks.push(new Firework(x, y));
|
||||
}
|
||||
|
||||
// Update active fireworks - only if not too many
|
||||
const fireLimit = Math.min(fireworks.length, isReducedEffects ? 10 : fireworks.length);
|
||||
for (let i = 0; i < fireLimit; i++) {
|
||||
fireworks[i].update();
|
||||
}
|
||||
}
|
||||
|
||||
// Request the next frame
|
||||
// Continue the animation
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
// Draw game elements
|
||||
// Draw game elements with frame limiting
|
||||
function draw() {
|
||||
// Clear the entire canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (gameOver || paused) return;
|
||||
|
||||
// Draw a border around the play area
|
||||
ctx.strokeStyle = 'rgba(80, 80, 80, 0.6)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(0, 0, canvas.width, canvas.height);
|
||||
// Frame rate limiting
|
||||
const now = performance.now();
|
||||
const elapsed = now - lastTimestamp;
|
||||
|
||||
// Clear any previous piece positions
|
||||
clearPreviousPiecePosition();
|
||||
// FPS monitoring
|
||||
frameCounter++;
|
||||
if (now - lastFpsUpdate > 1000) {
|
||||
currentFps = frameCounter;
|
||||
frameCounter = 0;
|
||||
lastFpsUpdate = now;
|
||||
|
||||
// Log FPS every second for debugging
|
||||
if (isReducedEffects) {
|
||||
console.log(`Current FPS: ${currentFps}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip frames to maintain target FPS
|
||||
const frameTime = isMobile ? (1000 / MOBILE_FPS_LIMIT) : FRAME_MIN_TIME;
|
||||
if (elapsed < frameTime) {
|
||||
requestAnimationFrame(draw);
|
||||
return;
|
||||
}
|
||||
|
||||
lastTimestamp = now - (elapsed % frameTime);
|
||||
|
||||
// Only clear what's needed
|
||||
ctx.fillStyle = EMPTY;
|
||||
ctx.fillRect(0, 0, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
|
||||
|
||||
// Draw the board - this shows only locked pieces
|
||||
drawBoard();
|
||||
|
@ -1603,17 +1667,23 @@ function draw() {
|
|||
p.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();
|
||||
// On mobile, render fireworks less frequently
|
||||
if (!isMobile || frameCounter % 2 === 0) {
|
||||
// Draw fireworks (with clipping for performance)
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, canvas.width, canvas.height);
|
||||
ctx.clip();
|
||||
|
||||
// Limit how many fireworks we draw
|
||||
const fireworkLimit = isReducedEffects ? 5 : fireworks.length;
|
||||
for (let i = 0; i < Math.min(fireworkLimit, fireworks.length); i++) {
|
||||
fireworks[i].draw();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Draw messages if game is paused
|
||||
// Draw paused message if needed
|
||||
if (paused) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
@ -1623,609 +1693,13 @@ function draw() {
|
|||
ctx.fillText('PAUSED', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
|
||||
// Request the next frame
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
// Drop the piece - called by interval
|
||||
function dropPiece() {
|
||||
if (gameOver || paused) return;
|
||||
|
||||
let now = Date.now();
|
||||
let delta = now - dropStart;
|
||||
|
||||
// Drop speed depends on level
|
||||
let speed = 1000 * (1 - (level - 1) * 0.1);
|
||||
speed = Math.max(speed, 100); // Don't allow too fast drops (minimum 100ms)
|
||||
|
||||
if (delta > speed) {
|
||||
p.moveDown();
|
||||
dropStart = now;
|
||||
|
||||
// If we can't move down and we're still at the top, game over
|
||||
if (p.collision(0, 1) && p.y < 1) {
|
||||
gameOver = true;
|
||||
showGameOver();
|
||||
}
|
||||
}
|
||||
|
||||
if (!gameOver) requestAnimationFrame(draw);
|
||||
}
|
||||
// Optimize touch events handling to avoid excessive processing
|
||||
// Add debounce for touch events
|
||||
let touchMoveDebounce = false;
|
||||
|
||||
// Show game over modal
|
||||
function showGameOver() {
|
||||
clearInterval(gameInterval);
|
||||
finalScoreElement.textContent = score;
|
||||
gameOverModal.classList.add('active');
|
||||
}
|
||||
|
||||
// Reset the game
|
||||
function resetGame() {
|
||||
// Reset game variables
|
||||
score = 0;
|
||||
level = 1;
|
||||
lines = 0;
|
||||
gameOver = false;
|
||||
paused = false;
|
||||
|
||||
// Clear the board
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
board[r][c] = EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI
|
||||
scoreElement.textContent = score;
|
||||
levelElement.textContent = level;
|
||||
linesElement.textContent = lines;
|
||||
|
||||
// Close game over modal if open
|
||||
gameOverModal.classList.remove('active');
|
||||
|
||||
// Generate pieces
|
||||
p = randomPiece();
|
||||
nextPiece = randomPiece();
|
||||
nextPiece.drawNextPiece();
|
||||
|
||||
// Start the game interval
|
||||
dropStart = Date.now();
|
||||
clearInterval(gameInterval);
|
||||
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')) {
|
||||
optionsModal.classList.remove('active');
|
||||
} else {
|
||||
optionsModal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply options settings
|
||||
function applyOptions() {
|
||||
enable3DEffects = toggle3DEffects.checked;
|
||||
enableSpinAnimations = toggleSpinAnimations.checked;
|
||||
animationSpeed = parseFloat(animationSpeedSlider.value);
|
||||
forceMobileControls = toggleMobileControls.checked;
|
||||
|
||||
// Apply mobile controls if checked or if on mobile device
|
||||
if (forceMobileControls) {
|
||||
if (!touchControls) {
|
||||
initTouchControls();
|
||||
touchControls = true;
|
||||
}
|
||||
document.body.classList.add('mobile-mode');
|
||||
} else if (!isMobile) {
|
||||
document.body.classList.remove('mobile-mode');
|
||||
}
|
||||
|
||||
// Save options
|
||||
saveOptions();
|
||||
}
|
||||
|
||||
// Save options to localStorage
|
||||
function saveOptions() {
|
||||
localStorage.setItem('tetris3DOptions', JSON.stringify({
|
||||
enable3DEffects,
|
||||
enableSpinAnimations,
|
||||
animationSpeed,
|
||||
forceMobileControls
|
||||
}));
|
||||
}
|
||||
|
||||
// Load options from localStorage
|
||||
function loadOptions() {
|
||||
const savedOptions = localStorage.getItem('tetris3DOptions');
|
||||
if (savedOptions) {
|
||||
const options = JSON.parse(savedOptions);
|
||||
|
||||
enable3DEffects = options.enable3DEffects !== undefined ? options.enable3DEffects : true;
|
||||
enableSpinAnimations = options.enableSpinAnimations !== undefined ? options.enableSpinAnimations : true;
|
||||
animationSpeed = options.animationSpeed !== undefined ? options.animationSpeed : 0.05;
|
||||
forceMobileControls = options.forceMobileControls !== undefined ? options.forceMobileControls : false;
|
||||
|
||||
// Update UI controls
|
||||
toggle3DEffects.checked = enable3DEffects;
|
||||
toggleSpinAnimations.checked = enableSpinAnimations;
|
||||
animationSpeedSlider.value = animationSpeed;
|
||||
|
||||
if (toggleMobileControls) {
|
||||
toggleMobileControls.checked = forceMobileControls;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize game
|
||||
function init() {
|
||||
// Reset canvas to ensure clean state
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Load saved options
|
||||
loadOptions();
|
||||
|
||||
// Draw the board
|
||||
drawBoard();
|
||||
|
||||
// Generate initial pieces
|
||||
p = randomPiece();
|
||||
nextPiece = randomPiece();
|
||||
|
||||
// Draw initial next piece
|
||||
nextPiece.drawNextPiece();
|
||||
|
||||
// Set up game interval
|
||||
dropStart = Date.now();
|
||||
gameInterval = setInterval(dropPiece, 1000);
|
||||
|
||||
// Listen for keyboard events
|
||||
document.addEventListener('keydown', control);
|
||||
|
||||
// Add resize listener for responsive layout
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Initial resize to set correct dimensions
|
||||
handleResize();
|
||||
|
||||
// Button event listeners
|
||||
startBtn.addEventListener('click', resetGame);
|
||||
pauseBtn.addEventListener('click', togglePause);
|
||||
playAgainBtn.addEventListener('click', resetGame);
|
||||
shadowBtn.addEventListener('click', toggleShadow);
|
||||
optionsBtn.addEventListener('click', toggleOptionsModal);
|
||||
optionsCloseBtn.addEventListener('click', toggleOptionsModal);
|
||||
|
||||
// Options event listeners
|
||||
toggle3DEffects.addEventListener('change', function() {
|
||||
applyOptions();
|
||||
});
|
||||
|
||||
toggleSpinAnimations.addEventListener('change', function() {
|
||||
applyOptions();
|
||||
});
|
||||
|
||||
animationSpeedSlider.addEventListener('input', function() {
|
||||
applyOptions();
|
||||
});
|
||||
|
||||
if (toggleMobileControls) {
|
||||
toggleMobileControls.addEventListener('change', function() {
|
||||
applyOptions();
|
||||
|
||||
// Force resize to update layout
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
}
|
||||
|
||||
// Apply mobile mode if forced or on mobile device
|
||||
if (forceMobileControls || isMobile) {
|
||||
if (!touchControls) {
|
||||
initTouchControls();
|
||||
touchControls = true;
|
||||
}
|
||||
document.body.classList.add('mobile-mode');
|
||||
}
|
||||
|
||||
// Initialize controller support
|
||||
initControllerSupport();
|
||||
|
||||
// Initial controller status update
|
||||
updateControllerStatus();
|
||||
}
|
||||
|
||||
// Handle window resize for responsive layout
|
||||
function handleResize() {
|
||||
const gameContainer = document.querySelector('.game-container');
|
||||
const gameWrapper = document.querySelector('.game-wrapper');
|
||||
const scoreContainer = document.querySelector('.score-container');
|
||||
|
||||
if (isMobile || forceMobileControls) {
|
||||
// Scale the canvas to fit mobile screen
|
||||
const viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
||||
const viewportHeight = Math.min(window.innerHeight, document.documentElement.clientHeight);
|
||||
|
||||
// Detect orientation
|
||||
const isPortrait = viewportHeight > viewportWidth;
|
||||
|
||||
// Calculate available game area (accounting for UI elements)
|
||||
const titleHeight = 40; // Estimate for title
|
||||
const scoreWidth = isPortrait ? 120 : 100; // Width for score container in portrait/landscape
|
||||
const availableWidth = viewportWidth - scoreWidth - 20; // Subtract score width + padding
|
||||
const availableHeight = viewportHeight - titleHeight - 20; // Subtract title height + padding
|
||||
|
||||
// Calculate optimal dimensions while maintaining aspect ratio
|
||||
const gameRatio = ROWS / COLS;
|
||||
|
||||
// Calculate scale based on available space
|
||||
let targetWidth, targetHeight;
|
||||
|
||||
if (isPortrait) {
|
||||
// For portrait, prioritize fitting the width
|
||||
targetWidth = availableWidth * 0.95;
|
||||
targetHeight = targetWidth * gameRatio;
|
||||
|
||||
// If too tall, scale down based on height
|
||||
if (targetHeight > availableHeight * 0.95) {
|
||||
targetHeight = availableHeight * 0.95;
|
||||
targetWidth = targetHeight / gameRatio;
|
||||
}
|
||||
} else {
|
||||
// For landscape, prioritize fitting the height
|
||||
targetHeight = availableHeight * 0.95;
|
||||
targetWidth = targetHeight / gameRatio;
|
||||
|
||||
// If too wide, scale down based on width
|
||||
if (targetWidth > availableWidth * 0.95) {
|
||||
targetWidth = availableWidth * 0.95;
|
||||
targetHeight = targetWidth * gameRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply dimensions
|
||||
canvas.style.width = `${targetWidth}px`;
|
||||
canvas.style.height = `${targetHeight}px`;
|
||||
|
||||
// Force a redraw of the nextPiece preview to fix rendering issues
|
||||
if (nextPiece) {
|
||||
setTimeout(() => {
|
||||
nextPieceCtx.clearRect(0, 0, nextPieceCanvas.width, nextPieceCanvas.height);
|
||||
nextPiece.drawNextPiece();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Show touch controls mode
|
||||
document.body.classList.add('mobile-mode');
|
||||
} else {
|
||||
// Reset to desktop layout
|
||||
canvas.style.width = '';
|
||||
canvas.style.height = '';
|
||||
|
||||
document.body.classList.remove('mobile-mode');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle shadow display
|
||||
function toggleShadow() {
|
||||
showShadow = !showShadow;
|
||||
if (!gameOver && !paused) {
|
||||
// Redraw current piece to show/hide shadow
|
||||
p.undraw();
|
||||
p.draw();
|
||||
}
|
||||
}
|
||||
|
||||
// Controller support
|
||||
function initControllerSupport() {
|
||||
// Check for existing gamepads
|
||||
scanGamepads();
|
||||
|
||||
// Listen for controller connection/disconnection
|
||||
window.addEventListener('gamepadconnected', (e) => {
|
||||
console.log('Gamepad connected:', e.gamepad.id);
|
||||
gamepadConnected = true;
|
||||
controllers[e.gamepad.index] = e.gamepad;
|
||||
|
||||
// Start polling for controller input if not already
|
||||
if (!controllerInterval) {
|
||||
controllerInterval = setInterval(pollControllers, controllerPollingRate);
|
||||
}
|
||||
|
||||
// Show controller connected message
|
||||
showControllerMessage(`Controller connected: ${e.gamepad.id}`);
|
||||
});
|
||||
|
||||
window.addEventListener('gamepaddisconnected', (e) => {
|
||||
console.log('Gamepad disconnected:', e.gamepad.id);
|
||||
delete controllers[e.gamepad.index];
|
||||
|
||||
// Check if there are any controllers left
|
||||
if (Object.keys(controllers).length === 0) {
|
||||
gamepadConnected = false;
|
||||
clearInterval(controllerInterval);
|
||||
controllerInterval = null;
|
||||
}
|
||||
|
||||
// Show controller disconnected message
|
||||
showControllerMessage('Controller disconnected');
|
||||
});
|
||||
|
||||
// Initial scan
|
||||
if (Object.keys(controllers).length > 0) {
|
||||
controllerInterval = setInterval(pollControllers, controllerPollingRate);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for connected gamepads
|
||||
function scanGamepads() {
|
||||
const gamepads = navigator.getGamepads ? navigator.getGamepads() :
|
||||
(navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
|
||||
|
||||
for (let i = 0; i < gamepads.length; i++) {
|
||||
if (gamepads[i]) {
|
||||
controllers[gamepads[i].index] = gamepads[i];
|
||||
gamepadConnected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll controllers for input
|
||||
function pollControllers() {
|
||||
if (!gamepadConnected || gameOver) return;
|
||||
|
||||
// Get fresh gamepad data
|
||||
scanGamepads();
|
||||
|
||||
// Use the first available controller
|
||||
const controller = controllers[Object.keys(controllers)[0]];
|
||||
if (!controller) return;
|
||||
|
||||
// Initialize state object if needed
|
||||
if (!lastControllerState[controller.index]) {
|
||||
lastControllerState[controller.index] = {
|
||||
buttons: Array(controller.buttons.length).fill(false),
|
||||
axes: Array(controller.axes.length).fill(0)
|
||||
};
|
||||
}
|
||||
|
||||
// Check buttons
|
||||
for (let action in controllerMapping) {
|
||||
const buttonIndex = controllerMapping[action][0];
|
||||
|
||||
// Handle button presses
|
||||
if (buttonIndex >= 0 && buttonIndex < controller.buttons.length) {
|
||||
const button = controller.buttons[buttonIndex];
|
||||
const pressed = button.pressed || button.value > 0.5;
|
||||
|
||||
// Check if this is a new press (wasn't pressed last time)
|
||||
if (pressed && !lastControllerState[controller.index].buttons[buttonIndex]) {
|
||||
// Handle controller action
|
||||
handleControllerAction(action);
|
||||
}
|
||||
|
||||
// Update state
|
||||
lastControllerState[controller.index].buttons[buttonIndex] = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle analog stick for movement
|
||||
// Left stick horizontal
|
||||
if (controller.axes[0] < -0.5 && lastControllerState[controller.index].axes[0] >= -0.5) {
|
||||
handleControllerAction('left');
|
||||
}
|
||||
else if (controller.axes[0] > 0.5 && lastControllerState[controller.index].axes[0] <= 0.5) {
|
||||
handleControllerAction('right');
|
||||
}
|
||||
|
||||
// Left stick vertical
|
||||
if (controller.axes[1] > 0.5 && lastControllerState[controller.index].axes[1] <= 0.5) {
|
||||
handleControllerAction('down');
|
||||
}
|
||||
|
||||
// Update axes state
|
||||
lastControllerState[controller.index].axes = [...controller.axes];
|
||||
}
|
||||
|
||||
// Handle controller actions
|
||||
function handleControllerAction(action) {
|
||||
if (paused && action !== 'pause') return;
|
||||
|
||||
switch(action) {
|
||||
case 'left':
|
||||
p.moveLeft();
|
||||
break;
|
||||
case 'right':
|
||||
p.moveRight();
|
||||
break;
|
||||
case 'down':
|
||||
p.moveDown();
|
||||
break;
|
||||
case 'rotateLeft':
|
||||
p.rotate('left');
|
||||
break;
|
||||
case 'rotateRight':
|
||||
p.rotate('right');
|
||||
break;
|
||||
case 'mirrorH':
|
||||
p.mirrorHorizontal();
|
||||
break;
|
||||
case 'mirrorV':
|
||||
p.mirrorVertical();
|
||||
break;
|
||||
case 'hardDrop':
|
||||
p.hardDrop();
|
||||
break;
|
||||
case 'pause':
|
||||
togglePause();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show controller message
|
||||
function showControllerMessage(message) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'controller-message';
|
||||
messageDiv.textContent = message;
|
||||
document.body.appendChild(messageDiv);
|
||||
|
||||
// Remove after a delay
|
||||
setTimeout(() => {
|
||||
messageDiv.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(messageDiv);
|
||||
}, 500);
|
||||
}, 3000);
|
||||
|
||||
// Update controller status indicator
|
||||
updateControllerStatus();
|
||||
}
|
||||
|
||||
// Update controller status indicator
|
||||
function updateControllerStatus() {
|
||||
if (gamepadConnected && Object.keys(controllers).length > 0) {
|
||||
const controller = controllers[Object.keys(controllers)[0]];
|
||||
controllerStatus.textContent = `Controller Connected: ${controller.id.slice(0, 20)}...`;
|
||||
controllerStatus.classList.remove('disconnected');
|
||||
controllerStatus.classList.add('connected');
|
||||
} else {
|
||||
controllerStatus.textContent = 'No Controller Detected';
|
||||
controllerStatus.classList.remove('connected');
|
||||
controllerStatus.classList.add('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
// The control function
|
||||
function control(event) {
|
||||
if (gameOver) return;
|
||||
if (paused && event.keyCode !== 80) return; // Allow only P key if paused
|
||||
|
||||
switch(event.keyCode) {
|
||||
case 37: // Left arrow
|
||||
case 65: // A key
|
||||
p.moveLeft();
|
||||
break;
|
||||
case 38: // Up arrow - no function
|
||||
break;
|
||||
case 39: // Right arrow
|
||||
case 68: // D key
|
||||
p.moveRight();
|
||||
break;
|
||||
case 40: // Down arrow
|
||||
case 83: // S key
|
||||
p.moveDown();
|
||||
break;
|
||||
case 81: // Q key - rotate left
|
||||
p.rotate('left');
|
||||
break;
|
||||
case 69: // E key - rotate right
|
||||
p.rotate('right');
|
||||
break;
|
||||
case 87: // W key - 3D vertical rotation
|
||||
p.rotate3DX();
|
||||
break;
|
||||
case 88: // X key - 3D horizontal rotation
|
||||
p.rotate3DY();
|
||||
break;
|
||||
case 32: // Space bar - hard drop
|
||||
p.hardDrop();
|
||||
break;
|
||||
case 80: // P key - pause
|
||||
togglePause();
|
||||
break;
|
||||
case 72: // H key - toggle shadow
|
||||
toggleShadow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain a reference to the active piece's previous position for proper clearing
|
||||
let previousPiecePosition = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
shape: null,
|
||||
exists: false
|
||||
};
|
||||
|
||||
// Always clear previous piece position before drawing new position
|
||||
function clearPreviousPiecePosition() {
|
||||
if (previousPiecePosition.exists && previousPiecePosition.shape) {
|
||||
// Clear entire previous area with padding
|
||||
const padding = 5;
|
||||
const minX = Math.max(0, previousPiecePosition.x - padding);
|
||||
const maxX = Math.min(COLS - 1, previousPiecePosition.x + previousPiecePosition.shape[0].length + padding);
|
||||
const minY = Math.max(0, previousPiecePosition.y - padding);
|
||||
const maxY = Math.min(ROWS - 1, previousPiecePosition.y + previousPiecePosition.shape.length + padding);
|
||||
|
||||
for (let r = minY; r <= maxY; r++) {
|
||||
for (let c = minX; c <= maxX; c++) {
|
||||
if (r >= 0 && c >= 0 && r < ROWS && c < COLS && board[r][c] === EMPTY) {
|
||||
drawSquare(c, r, EMPTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Touch control variables
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchStartTime = 0;
|
||||
const SWIPE_THRESHOLD = 15; // Reduced threshold for more responsive movement
|
||||
const TAP_THRESHOLD = 200; // milliseconds
|
||||
const DOUBLE_TAP_THRESHOLD = 300; // Reduced from 400 to make double-tap easier
|
||||
const TRIPLE_TAP_THRESHOLD = 600; // Maximum time between first and third tap
|
||||
let lastTapTime = 0;
|
||||
let secondLastTapTime = 0; // Track time of second-to-last tap for triple tap detection
|
||||
let lastMoveTime = 0;
|
||||
let touchIdentifier = null;
|
||||
let lastTapX = 0;
|
||||
let lastTapY = 0;
|
||||
let secondLastTapX = 0; // Track position of second-to-last tap
|
||||
let secondLastTapY = 0; // Track position of second-to-last tap
|
||||
const TAP_DISTANCE_THRESHOLD = 40; // Increased from 20 to be more forgiving for double-taps
|
||||
const MOVE_COOLDOWN = 60; // Cooldown between moves to prevent too rapid movement
|
||||
|
||||
// Initialize touch controls
|
||||
function initTouchControls() {
|
||||
// Add touch event listeners to the canvas
|
||||
canvas.addEventListener('touchstart', handleTouchStart, false);
|
||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
canvas.addEventListener('touchend', handleTouchEnd, false);
|
||||
|
||||
// Create on-screen control buttons
|
||||
createTouchControlButtons();
|
||||
}
|
||||
|
||||
// Handle touch start event
|
||||
function handleTouchStart(event) {
|
||||
if (gameOver || paused) return;
|
||||
event.preventDefault();
|
||||
|
||||
// Only track single touches for gesture detection
|
||||
if (event.touches.length > 1) return;
|
||||
|
||||
// Store the initial touch position
|
||||
const touch = event.touches[0];
|
||||
touchStartX = touch.clientX;
|
||||
touchStartY = touch.clientY;
|
||||
touchStartTime = Date.now();
|
||||
touchIdentifier = touch.identifier;
|
||||
}
|
||||
|
||||
// Handle touch move event
|
||||
function handleTouchMove(event) {
|
||||
if (gameOver || paused) return;
|
||||
event.preventDefault();
|
||||
|
@ -2233,6 +1707,11 @@ function handleTouchMove(event) {
|
|||
// Skip if it's a multi-touch gesture
|
||||
if (event.touches.length > 1) return;
|
||||
|
||||
// Add debounce to reduce excessive processing
|
||||
if (touchMoveDebounce) return;
|
||||
touchMoveDebounce = true;
|
||||
setTimeout(() => { touchMoveDebounce = false; }, 16); // ~60fps
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (!event.touches.length) return;
|
||||
|
@ -2272,6 +1751,22 @@ function handleTouchMove(event) {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle touch start event
|
||||
function handleTouchStart(event) {
|
||||
if (gameOver || paused) return;
|
||||
event.preventDefault();
|
||||
|
||||
// Only track single touches for gesture detection
|
||||
if (event.touches.length > 1) return;
|
||||
|
||||
// Store the initial touch position
|
||||
const touch = event.touches[0];
|
||||
touchStartX = touch.clientX;
|
||||
touchStartY = touch.clientY;
|
||||
touchStartTime = Date.now();
|
||||
touchIdentifier = touch.identifier;
|
||||
}
|
||||
|
||||
// Handle touch end event
|
||||
function handleTouchEnd(event) {
|
||||
if (gameOver || paused) return;
|
||||
|
@ -2504,7 +1999,13 @@ function getNextPieceFromBag() {
|
|||
|
||||
// Start the game
|
||||
window.onload = function() {
|
||||
// Initialize the game
|
||||
init();
|
||||
|
||||
// Apply mobile optimizations
|
||||
optimizeForMobile();
|
||||
|
||||
// Start animation loops
|
||||
update(); // Start the fireworks update loop
|
||||
draw(); // Start the drawing loop
|
||||
|
||||
|
@ -2516,4 +2017,38 @@ window.onload = function() {
|
|||
// Force resize to ensure proper mobile layout
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Add a mobile-specific optimization function to call on game initialization
|
||||
function optimizeForMobile() {
|
||||
if (isMobile) {
|
||||
// Reduce effects
|
||||
isReducedEffects = true;
|
||||
maxFireworks = 5;
|
||||
maxParticlesPerFirework = 10;
|
||||
|
||||
// Use lower frame rate for mobile
|
||||
const FRAME_MIN_TIME = (1000 / MOBILE_FPS_LIMIT);
|
||||
|
||||
// Reduce shadow complexity
|
||||
showShadow = false; // Start with shadow off on mobile for performance
|
||||
|
||||
console.log("Applying mobile optimizations");
|
||||
}
|
||||
}
|
||||
|
||||
// Add cleanup function to prevent memory leaks
|
||||
function cleanup() {
|
||||
// Clear any large arrays that might be causing memory issues
|
||||
fireworks = fireworks.slice(0, Math.min(fireworks.length, maxFireworks));
|
||||
|
||||
// Force garbage collection hints
|
||||
if (window.gc) window.gc();
|
||||
|
||||
console.log("Memory cleanup performed");
|
||||
}
|
||||
|
||||
// Call cleanup periodically on mobile devices
|
||||
if (isMobile) {
|
||||
setInterval(cleanup, 60000); // Cleanup every minute
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue