mirror of
https://github.com/cmclark00/tetris-3d.git
synced 2025-05-17 15:15:20 +01:00
Fix rotation issues and optimize for mobile devices
This commit is contained in:
parent
74565de189
commit
7d45fc36fb
3 changed files with 500 additions and 294 deletions
617
script.js
617
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 = `
|
||||
<button id="rotate3d-x" class="rotate-btn">3D Flip ↕</button>
|
||||
<button id="rotate3d-y" class="rotate-btn">3D Flip ↔</button>
|
||||
`;
|
||||
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() {
|
|||
<p><b>Swipe down:</b> Soft drop</p>
|
||||
<p><b>Tap anywhere:</b> Rotate right</p>
|
||||
<p><b>Double-tap:</b> Hard drop</p>
|
||||
<p><b>Triple-tap:</b> 3D rotate</p>
|
||||
<p><b>3D Flip Buttons:</b> Rotate in 3D</p>
|
||||
`;
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue