mirror of
https://github.com/cmclark00/tetris-3d.git
synced 2025-05-17 23:25:21 +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
38
index.html
38
index.html
|
@ -80,32 +80,34 @@
|
||||||
<!-- Options Modal -->
|
<!-- Options Modal -->
|
||||||
<div id="options-modal" class="modal">
|
<div id="options-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h2>OPTIONS</h2>
|
<h2>Game Options</h2>
|
||||||
|
|
||||||
<div class="option-row">
|
<div class="option-row">
|
||||||
<label>3D Effects: </label>
|
<label for="toggle-3d-effects">Enable 3D Effects</label>
|
||||||
<label class="switch">
|
<input type="checkbox" id="toggle-3d-effects" checked>
|
||||||
<input type="checkbox" id="toggle-3d-effects" checked>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="option-row">
|
<div class="option-row">
|
||||||
<label>Spin Animations: </label>
|
<label for="toggle-spin-animations">Enable Spin Animations</label>
|
||||||
<label class="switch">
|
<input type="checkbox" id="toggle-spin-animations" checked>
|
||||||
<input type="checkbox" id="toggle-spin-animations" checked>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="option-row">
|
<div class="option-row">
|
||||||
<label>Animation Speed: </label>
|
<label for="animation-speed">Animation Speed</label>
|
||||||
<input type="range" id="animation-speed" min="0.01" max="0.2" step="0.01" value="0.05">
|
<input type="range" id="animation-speed" min="0.01" max="0.1" step="0.01" value="0.05">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="option-row mobile-only">
|
||||||
|
<label for="toggle-mobile-controls">Force Mobile Controls</label>
|
||||||
|
<input type="checkbox" id="toggle-mobile-controls">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="option-row">
|
<div class="option-row">
|
||||||
<label>Mobile Controls: </label>
|
<label for="toggle-mobile-performance">Performance Mode</label>
|
||||||
<label class="switch">
|
<input type="checkbox" id="toggle-mobile-performance">
|
||||||
<input type="checkbox" id="toggle-mobile-controls">
|
<span class="tooltip">Reduces visual effects for better performance on mobile devices</span>
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="options-close-btn" class="game-btn">Close</button>
|
<button id="options-close-btn" class="game-btn">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
617
script.js
617
script.js
|
@ -12,6 +12,8 @@ const PREVIEW_BLOCK_SIZE = 25;
|
||||||
// Mobile detection
|
// Mobile detection
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
let touchControls = false;
|
let touchControls = false;
|
||||||
|
// Mobile performance mode to reduce effects on mobile
|
||||||
|
let mobilePerformanceMode = isMobile;
|
||||||
|
|
||||||
// 7-bag randomization variables
|
// 7-bag randomization variables
|
||||||
let pieceBag = [];
|
let pieceBag = [];
|
||||||
|
@ -67,6 +69,7 @@ let enable3DEffects = true;
|
||||||
let enableSpinAnimations = true;
|
let enableSpinAnimations = true;
|
||||||
let animationSpeed = 0.05;
|
let animationSpeed = 0.05;
|
||||||
let forceMobileControls = false;
|
let forceMobileControls = false;
|
||||||
|
let reduceEffectsOnMobile = true; // New option to reduce effects on mobile
|
||||||
|
|
||||||
// Controller variables
|
// Controller variables
|
||||||
let gamepadConnected = false;
|
let gamepadConnected = false;
|
||||||
|
@ -317,26 +320,19 @@ class Firework {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.particles = [];
|
this.particles = [];
|
||||||
this.particleCount = 50;
|
|
||||||
this.gravity = 0.2;
|
|
||||||
this.isDone = false;
|
this.isDone = false;
|
||||||
this.colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'];
|
|
||||||
|
|
||||||
// Create particles
|
// Create particles
|
||||||
for (let i = 0; i < this.particleCount; i++) {
|
const particleCount = mobilePerformanceMode ? 15 : 30;
|
||||||
const angle = Math.random() * Math.PI * 2;
|
|
||||||
const speed = Math.random() * 3 + 2;
|
for (let i = 0; i < particleCount; i++) {
|
||||||
const size = Math.random() * 3 + 1;
|
|
||||||
const color = this.colors[Math.floor(Math.random() * this.colors.length)];
|
|
||||||
|
|
||||||
this.particles.push({
|
this.particles.push({
|
||||||
x: this.x,
|
x: this.x,
|
||||||
y: this.y,
|
y: this.y,
|
||||||
vx: Math.cos(angle) * speed,
|
vx: (Math.random() - 0.5) * 8,
|
||||||
vy: Math.sin(angle) * speed,
|
vy: (Math.random() - 0.5) * 8,
|
||||||
size: size,
|
alpha: 1,
|
||||||
color: color,
|
color: `hsl(${Math.random() * 360}, 100%, 50%)`
|
||||||
alpha: 1
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -352,11 +348,12 @@ class Firework {
|
||||||
p.y += p.vy;
|
p.y += p.vy;
|
||||||
|
|
||||||
// Apply gravity
|
// Apply gravity
|
||||||
p.vy += this.gravity;
|
p.vy += 0.1;
|
||||||
|
|
||||||
// Reduce alpha (fade out)
|
// Fade out
|
||||||
p.alpha -= 0.01;
|
p.alpha -= 0.02;
|
||||||
|
|
||||||
|
// Check if any particles are still visible
|
||||||
if (p.alpha > 0) {
|
if (p.alpha > 0) {
|
||||||
allDone = false;
|
allDone = false;
|
||||||
}
|
}
|
||||||
|
@ -371,21 +368,22 @@ class Firework {
|
||||||
|
|
||||||
if (p.alpha <= 0) continue;
|
if (p.alpha <= 0) continue;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
ctx.globalAlpha = p.alpha;
|
ctx.globalAlpha = p.alpha;
|
||||||
ctx.fillStyle = p.color;
|
ctx.fillStyle = p.color;
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
// Draw smaller particles on mobile for better performance
|
||||||
ctx.closePath();
|
const size = mobilePerformanceMode ? 3 : 5;
|
||||||
ctx.fill();
|
ctx.fillRect(p.x, p.y, size, size);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.globalAlpha = 1; // Reset alpha
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Piece class
|
// The Piece class
|
||||||
class Piece {
|
class Piece {
|
||||||
constructor(tetromino, tetrominoN, color) {
|
constructor(tetromino, tetrominoN, color, tetrominoType) {
|
||||||
this.tetromino = tetromino;
|
this.tetromino = tetromino;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
|
|
||||||
|
@ -393,6 +391,9 @@ class Piece {
|
||||||
this.activeTetromino = this.tetromino[this.tetrominoN];
|
this.activeTetromino = this.tetromino[this.tetrominoN];
|
||||||
this.shadowTetromino = this.activeTetromino; // For shadow calculation
|
this.shadowTetromino = this.activeTetromino; // For shadow calculation
|
||||||
|
|
||||||
|
// Store piece type for SRS rotation kicks
|
||||||
|
this.tetrominoType = tetrominoType;
|
||||||
|
|
||||||
// Starting position
|
// Starting position
|
||||||
this.x = 3;
|
this.x = 3;
|
||||||
this.y = -2;
|
this.y = -2;
|
||||||
|
@ -575,63 +576,86 @@ class Piece {
|
||||||
|
|
||||||
// Rotate with 3D animation
|
// Rotate with 3D animation
|
||||||
rotate(direction) {
|
rotate(direction) {
|
||||||
|
// Direction can be 'right' or 'left'
|
||||||
|
if (gameOver || paused) return;
|
||||||
|
|
||||||
// Clear previous position
|
// Clear previous position
|
||||||
this.undraw();
|
this.undraw();
|
||||||
clearPreviousPiecePosition();
|
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;
|
let nextPattern;
|
||||||
|
|
||||||
if (direction === 'right') {
|
if (direction === 'right') {
|
||||||
nextPattern = (this.tetrominoN + 1) % this.tetromino.length;
|
// Rotate clockwise
|
||||||
} else {
|
if (this.tetrominoType === 'I') {
|
||||||
nextPattern = (this.tetrominoN - 1 + this.tetromino.length) % this.tetromino.length;
|
nextPattern = (this.tetrominoN + 1) % 2;
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
} else {
|
} else {
|
||||||
// Left wall collision
|
nextPattern = (this.tetrominoN + 1) % 4;
|
||||||
kick = 1;
|
}
|
||||||
|
} 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
|
// Wall kick testing for rotation
|
||||||
if (!enableSpinAnimations) {
|
// Try the standard position first, then the kickTable positions
|
||||||
// Apply pattern if not colliding
|
const kicks = this.getKicks(this.tetrominoN, nextPattern);
|
||||||
if (!this.collision(kick, 0, this.tetromino[nextPattern])) {
|
|
||||||
this.x += kick;
|
// Try each kick position until we find one that works
|
||||||
this.tetrominoN = nextPattern;
|
let validKick = null;
|
||||||
this.activeTetromino = this.tetromino[this.tetrominoN];
|
|
||||||
this.shadowTetromino = this.activeTetromino;
|
for (let i = 0; i < kicks.length; i++) {
|
||||||
|
const [kickX, kickY] = kicks[i];
|
||||||
// Update shadow position
|
|
||||||
this.calculateShadowY();
|
if (!this.collision(kickX, kickY, this.tetromino[nextPattern])) {
|
||||||
|
validKick = kickX;
|
||||||
// Play rotation sound
|
// If a successful position is found, break the loop
|
||||||
playPieceSound('rotate');
|
break;
|
||||||
|
|
||||||
// Draw the piece
|
|
||||||
this.draw();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid kick position found, keep the original position
|
||||||
|
if (validKick === null) {
|
||||||
|
this.draw();
|
||||||
return;
|
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.rotationTransition = true;
|
||||||
this.rotationDirection = direction === 'right' ? 'rotateRight' : 'rotateLeft';
|
this.rotationDirection = direction === 'right' ? 'rotateRight' : 'rotateLeft';
|
||||||
this.rotationProgress = 0;
|
this.rotationProgress = 0;
|
||||||
|
this.rotationEasing = true;
|
||||||
|
|
||||||
// Store current tetromino
|
// Store original tetromino for animation
|
||||||
this.originalTetromino = this.activeTetromino;
|
this.originalTetromino = this.activeTetromino;
|
||||||
|
|
||||||
// Set target tetrominoN and position
|
|
||||||
this.targetPattern = nextPattern;
|
|
||||||
this.targetKick = kick;
|
|
||||||
|
|
||||||
// Play rotation sound
|
// Play rotation sound
|
||||||
playPieceSound('rotate');
|
playPieceSound('rotate');
|
||||||
|
|
||||||
|
@ -813,11 +837,10 @@ class Piece {
|
||||||
lock() {
|
lock() {
|
||||||
for (let r = 0; r < this.activeTetromino.length; r++) {
|
for (let r = 0; r < this.activeTetromino.length; r++) {
|
||||||
for (let c = 0; c < this.activeTetromino[r].length; c++) {
|
for (let c = 0; c < this.activeTetromino[r].length; c++) {
|
||||||
if (!this.activeTetromino[r][c]) {
|
// Skip empty cells
|
||||||
continue;
|
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) {
|
if (this.y + r < 0) {
|
||||||
gameOver = true;
|
gameOver = true;
|
||||||
break;
|
break;
|
||||||
|
@ -828,41 +851,48 @@ class Piece {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove full rows and track their positions for fireworks
|
// Clear full rows
|
||||||
let linesCleared = 0;
|
let linesCleared = 0;
|
||||||
let clearedRows = [];
|
let clearedRows = [];
|
||||||
|
|
||||||
for (let r = 0; r < ROWS; r++) {
|
for (let r = 0; r < ROWS; r++) {
|
||||||
let isRowFull = true;
|
let isRowFull = true;
|
||||||
|
|
||||||
|
// Check if the row is full
|
||||||
for (let c = 0; c < COLS; c++) {
|
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) {
|
if (isRowFull) {
|
||||||
// Store the row index for fireworks
|
|
||||||
clearedRows.push(r);
|
clearedRows.push(r);
|
||||||
|
linesCleared++;
|
||||||
|
|
||||||
// Remove the row
|
// Shift rows down
|
||||||
for (let y = r; y > 1; y--) {
|
for (let y = r; y > 0; y--) {
|
||||||
for (let c = 0; c < COLS; c++) {
|
for (let c = 0; c < COLS; c++) {
|
||||||
board[y][c] = board[y-1][c];
|
board[y][c] = board[y-1][c];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top row
|
// Clear the top row
|
||||||
for (let c = 0; c < COLS; c++) {
|
for (let c = 0; c < COLS; c++) {
|
||||||
board[0][c] = EMPTY;
|
board[0][c] = EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
linesCleared++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create fireworks for each cleared row
|
// Create fireworks for each cleared row
|
||||||
if (clearedRows.length > 0) {
|
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++) {
|
for (let i = 0; i < clearedRows.length; i++) {
|
||||||
// Create multiple fireworks along the row
|
// 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 x = (Math.random() * COLS * BLOCK_SIZE) + BLOCK_SIZE/2;
|
||||||
const y = (clearedRows[i] * BLOCK_SIZE) + BLOCK_SIZE/2;
|
const y = (clearedRows[i] * BLOCK_SIZE) + BLOCK_SIZE/2;
|
||||||
fireworks.push(new Firework(x, y));
|
fireworks.push(new Firework(x, y));
|
||||||
|
@ -972,41 +1002,59 @@ class Piece {
|
||||||
animate3DRotation() {
|
animate3DRotation() {
|
||||||
if (!this.rotationTransition && !this.showCompletionEffect) return;
|
if (!this.rotationTransition && !this.showCompletionEffect) return;
|
||||||
|
|
||||||
|
// Faster animation on mobile for better performance
|
||||||
|
const animationStep = mobilePerformanceMode ? animationSpeed * 1.5 : animationSpeed;
|
||||||
|
|
||||||
if (this.rotationTransition) {
|
if (this.rotationTransition) {
|
||||||
// Increment progress - use the global animation speed
|
// Update rotation progress
|
||||||
this.rotationProgress += animationSpeed;
|
this.rotationProgress += animationStep;
|
||||||
|
|
||||||
// When rotation is complete
|
|
||||||
if (this.rotationProgress >= 1) {
|
if (this.rotationProgress >= 1) {
|
||||||
this.rotationTransition = false;
|
// Animation complete, apply the final result
|
||||||
this.showCompletionEffect = enable3DEffects; // Only show completion effect if 3D effects are enabled
|
// Check if the target tetromino would collide with anything
|
||||||
this.completionEffectProgress = 0;
|
if (!this.targetTetromino) {
|
||||||
|
// Handle rotation without target tetromino (for standard rotations)
|
||||||
// Apply the target pattern/tetromino
|
if (this.targetPattern !== undefined && this.targetKick !== undefined) {
|
||||||
if (this.rotationDirection === 'horizontal' || this.rotationDirection === 'vertical') {
|
// Apply the rotation if it doesn't cause collision
|
||||||
// Apply the mirror if it doesn't cause collision
|
if (!this.collision(this.targetKick, 0, this.tetromino[this.targetPattern])) {
|
||||||
if (!this.collision(0, 0, this.targetTetromino)) {
|
this.x += this.targetKick;
|
||||||
this.activeTetromino = this.targetTetromino;
|
this.tetrominoN = this.targetPattern;
|
||||||
this.shadowTetromino = this.activeTetromino;
|
this.activeTetromino = this.tetromino[this.tetrominoN];
|
||||||
}
|
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;
|
|
||||||
}
|
}
|
||||||
|
} 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();
|
this.calculateShadowY();
|
||||||
|
|
||||||
// If no completion effect, we're done
|
// Skip completion effect on mobile for performance
|
||||||
if (!this.showCompletionEffect) {
|
if (!mobilePerformanceMode && enable3DEffects) {
|
||||||
this.draw();
|
// Start completion effect
|
||||||
return;
|
this.showCompletionEffect = true;
|
||||||
|
this.completionEffectProgress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw in new position
|
||||||
|
this.draw();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate current rotation angles based on progress with easing
|
// Calculate current rotation angles based on progress with easing
|
||||||
|
@ -1029,7 +1077,9 @@ class Piece {
|
||||||
}
|
}
|
||||||
} else if (this.showCompletionEffect) {
|
} else if (this.showCompletionEffect) {
|
||||||
// Handle completion effect animation
|
// 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) {
|
if (this.completionEffectProgress >= 1) {
|
||||||
this.showCompletionEffect = false;
|
this.showCompletionEffect = false;
|
||||||
|
@ -1043,8 +1093,12 @@ class Piece {
|
||||||
// Redraw with current rotation
|
// Redraw with current rotation
|
||||||
this.draw();
|
this.draw();
|
||||||
|
|
||||||
// Continue animation
|
// Continue animation - use lower frame rate on mobile
|
||||||
requestAnimationFrame(() => this.animate3DRotation());
|
if (mobilePerformanceMode) {
|
||||||
|
setTimeout(() => this.animate3DRotation(), 16); // ~60fps
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(() => this.animate3DRotation());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New method to draw with 3D rotation effect
|
// New method to draw with 3D rotation effect
|
||||||
|
@ -1058,6 +1112,9 @@ class Piece {
|
||||||
// Sort blocks by depth for proper rendering
|
// Sort blocks by depth for proper rendering
|
||||||
let blocks = [];
|
let blocks = [];
|
||||||
|
|
||||||
|
// Reduce complexity on mobile devices
|
||||||
|
const skipGradientOnMobile = mobilePerformanceMode;
|
||||||
|
|
||||||
for (let r = 0; r < tetromino.length; r++) {
|
for (let r = 0; r < tetromino.length; r++) {
|
||||||
for (let c = 0; c < tetromino[r].length; c++) {
|
for (let c = 0; c < tetromino[r].length; c++) {
|
||||||
if (!tetromino[r][c]) continue;
|
if (!tetromino[r][c]) continue;
|
||||||
|
@ -1081,8 +1138,8 @@ class Piece {
|
||||||
transX = relX * Math.cos(angle);
|
transX = relX * Math.cos(angle);
|
||||||
depth = relX * Math.sin(angle);
|
depth = relX * Math.sin(angle);
|
||||||
|
|
||||||
// Add perspective effect
|
// Add perspective effect - less intense on mobile
|
||||||
const perspective = 0.2;
|
const perspective = mobilePerformanceMode ? 0.15 : 0.2;
|
||||||
const perspectiveScale = 1 + depth * perspective;
|
const perspectiveScale = 1 + depth * perspective;
|
||||||
transX /= perspectiveScale;
|
transX /= perspectiveScale;
|
||||||
transY /= perspectiveScale;
|
transY /= perspectiveScale;
|
||||||
|
@ -1096,13 +1153,13 @@ class Piece {
|
||||||
transY = relY * Math.cos(angle);
|
transY = relY * Math.cos(angle);
|
||||||
depth = relY * Math.sin(angle);
|
depth = relY * Math.sin(angle);
|
||||||
|
|
||||||
// Add perspective effect
|
// Add perspective effect - less intense on mobile
|
||||||
const perspective = 0.2;
|
const perspective = mobilePerformanceMode ? 0.15 : 0.2;
|
||||||
const perspectiveScale = 1 + depth * perspective;
|
const perspectiveScale = 1 + depth * perspective;
|
||||||
transX /= perspectiveScale;
|
transX /= perspectiveScale;
|
||||||
transY /= perspectiveScale;
|
transY /= perspectiveScale;
|
||||||
scale /= perspectiveScale;
|
scale /= perspectiveScale;
|
||||||
} else if (this.rotationDirection === 'rotateRight' || this.rotationDirection === 'rotateLeft') {
|
} else {
|
||||||
// Z-axis rotation for regular tetris rotations
|
// Z-axis rotation for regular tetris rotations
|
||||||
const angle = this.rotationAngleZ;
|
const angle = this.rotationAngleZ;
|
||||||
|
|
||||||
|
@ -1114,8 +1171,9 @@ class Piece {
|
||||||
scale = 1.0;
|
scale = 1.0;
|
||||||
|
|
||||||
// Add subtle depth effect based on rotation progress
|
// Add subtle depth effect based on rotation progress
|
||||||
|
// Reduce effect on mobile
|
||||||
const rotationProgress = Math.abs(angle) / (Math.PI * 2);
|
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
|
// Apply transformation
|
||||||
|
@ -1140,7 +1198,7 @@ class Piece {
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.scale > 0) {
|
if (block.scale > 0) {
|
||||||
// Apply scale to create 3D effect with depth
|
// 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
|
// Draw a shadow square on the board
|
||||||
function drawShadowSquare(x, y) {
|
function drawShadowSquare(x, y) {
|
||||||
if (y < 0) return; // Don't draw above the board
|
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
|
// Draw a 3D square with rotation effect
|
||||||
function draw3DSquare(x, y, color, scale, depth = 0) {
|
function draw3DSquare(x, y, color, scale, depth = 0, skipGradient = false) {
|
||||||
if (y < 0) return; // Don't draw above the board
|
// 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();
|
ctx.save();
|
||||||
|
|
||||||
// Calculate the position with scaling from center of block
|
// Draw the block
|
||||||
const centerX = (x + 0.5) * BLOCK_SIZE;
|
ctx.fillStyle = color;
|
||||||
const centerY = (y + 0.5) * BLOCK_SIZE;
|
ctx.fillRect(pixelX + offsetX, pixelY + offsetY, size, size);
|
||||||
const scaledSize = BLOCK_SIZE * Math.abs(scale);
|
|
||||||
const offsetX = centerX - scaledSize / 2;
|
|
||||||
const offsetY = centerY - scaledSize / 2;
|
|
||||||
|
|
||||||
// Create gradient for filled squares
|
// Apply gradient for 3D effect - skip for performance mode
|
||||||
const gradient = ctx.createLinearGradient(
|
if (!skipGradient) {
|
||||||
offsetX,
|
// Lighter side (light source from top-left)
|
||||||
offsetY,
|
const lightVal = Math.min(255, 150 + depth * 200);
|
||||||
offsetX + scaledSize,
|
ctx.fillStyle = `rgba(${lightVal}, ${lightVal}, ${lightVal}, 0.3)`;
|
||||||
offsetY + scaledSize
|
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
|
// Restore context
|
||||||
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
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1569,6 +1622,9 @@ function playLineClearSound(lineCount) {
|
||||||
|
|
||||||
// Update game frame
|
// Update game frame
|
||||||
function update() {
|
function update() {
|
||||||
|
// Limit fireworks on mobile for performance
|
||||||
|
const maxFireworks = mobilePerformanceMode ? 5 : 15;
|
||||||
|
|
||||||
// Update and draw fireworks
|
// Update and draw fireworks
|
||||||
for (let i = fireworks.length - 1; i >= 0; i--) {
|
for (let i = fireworks.length - 1; i >= 0; i--) {
|
||||||
fireworks[i].update();
|
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
|
// Request the next frame
|
||||||
requestAnimationFrame(update);
|
requestAnimationFrame(update);
|
||||||
}
|
}
|
||||||
|
@ -1604,14 +1665,16 @@ function draw() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw fireworks (clipped to canvas area)
|
// Draw fireworks (clipped to canvas area)
|
||||||
ctx.save();
|
if (!mobilePerformanceMode || fireworks.length < 3) {
|
||||||
ctx.beginPath();
|
ctx.save();
|
||||||
ctx.rect(0, 0, canvas.width, canvas.height);
|
ctx.beginPath();
|
||||||
ctx.clip();
|
ctx.rect(0, 0, canvas.width, canvas.height);
|
||||||
for (let i = 0; i < fireworks.length; i++) {
|
ctx.clip();
|
||||||
fireworks[i].draw();
|
for (let i = 0; i < fireworks.length; i++) {
|
||||||
|
fireworks[i].draw();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
// Draw messages if game is paused
|
// Draw messages if game is paused
|
||||||
if (paused) {
|
if (paused) {
|
||||||
|
@ -1734,6 +1797,17 @@ function applyOptions() {
|
||||||
document.body.classList.remove('mobile-mode');
|
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
|
// Save options
|
||||||
saveOptions();
|
saveOptions();
|
||||||
}
|
}
|
||||||
|
@ -1744,7 +1818,8 @@ function saveOptions() {
|
||||||
enable3DEffects,
|
enable3DEffects,
|
||||||
enableSpinAnimations,
|
enableSpinAnimations,
|
||||||
animationSpeed,
|
animationSpeed,
|
||||||
forceMobileControls
|
forceMobileControls,
|
||||||
|
reduceEffectsOnMobile
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1758,6 +1833,7 @@ function loadOptions() {
|
||||||
enableSpinAnimations = options.enableSpinAnimations !== undefined ? options.enableSpinAnimations : true;
|
enableSpinAnimations = options.enableSpinAnimations !== undefined ? options.enableSpinAnimations : true;
|
||||||
animationSpeed = options.animationSpeed !== undefined ? options.animationSpeed : 0.05;
|
animationSpeed = options.animationSpeed !== undefined ? options.animationSpeed : 0.05;
|
||||||
forceMobileControls = options.forceMobileControls !== undefined ? options.forceMobileControls : false;
|
forceMobileControls = options.forceMobileControls !== undefined ? options.forceMobileControls : false;
|
||||||
|
reduceEffectsOnMobile = options.reduceEffectsOnMobile !== undefined ? options.reduceEffectsOnMobile : true;
|
||||||
|
|
||||||
// Update UI controls
|
// Update UI controls
|
||||||
toggle3DEffects.checked = enable3DEffects;
|
toggle3DEffects.checked = enable3DEffects;
|
||||||
|
@ -1767,6 +1843,10 @@ function loadOptions() {
|
||||||
if (toggleMobileControls) {
|
if (toggleMobileControls) {
|
||||||
toggleMobileControls.checked = forceMobileControls;
|
toggleMobileControls.checked = forceMobileControls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('toggle-mobile-performance')) {
|
||||||
|
document.getElementById('toggle-mobile-performance').checked = reduceEffectsOnMobile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1822,6 +1902,13 @@ function init() {
|
||||||
applyOptions();
|
applyOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (document.getElementById('toggle-mobile-performance')) {
|
||||||
|
document.getElementById('toggle-mobile-performance').addEventListener('change', function() {
|
||||||
|
mobilePerformanceMode = this.checked;
|
||||||
|
applyOptions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (toggleMobileControls) {
|
if (toggleMobileControls) {
|
||||||
toggleMobileControls.addEventListener('change', function() {
|
toggleMobileControls.addEventListener('change', function() {
|
||||||
applyOptions();
|
applyOptions();
|
||||||
|
@ -2293,29 +2380,6 @@ function handleTouchEnd(event) {
|
||||||
Math.pow(touchY - lastTapY, 2)
|
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)
|
// Check for double tap (for hard drop)
|
||||||
const timeBetweenTaps = touchEndTime - lastTapTime;
|
const timeBetweenTaps = touchEndTime - lastTapTime;
|
||||||
|
|
||||||
|
@ -2323,13 +2387,8 @@ function handleTouchEnd(event) {
|
||||||
// This is a double-tap, do hard drop
|
// This is a double-tap, do hard drop
|
||||||
p.hardDrop();
|
p.hardDrop();
|
||||||
|
|
||||||
// Store for potential triple tap
|
// Reset tap tracking
|
||||||
secondLastTapTime = lastTapTime;
|
lastTapTime = 0;
|
||||||
secondLastTapX = lastTapX;
|
|
||||||
secondLastTapY = lastTapY;
|
|
||||||
lastTapTime = touchEndTime;
|
|
||||||
lastTapX = touchX;
|
|
||||||
lastTapY = touchY;
|
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
console.log("Double tap detected - hard drop");
|
console.log("Double tap detected - hard drop");
|
||||||
|
@ -2339,15 +2398,7 @@ function handleTouchEnd(event) {
|
||||||
// Single tap - rotates piece
|
// Single tap - rotates piece
|
||||||
p.rotate('right');
|
p.rotate('right');
|
||||||
|
|
||||||
// Update tracking for potential double/triple tap
|
// Update tracking for potential double tap
|
||||||
if (lastTapTime > 0) {
|
|
||||||
// Store previous tap data
|
|
||||||
secondLastTapTime = lastTapTime;
|
|
||||||
secondLastTapX = lastTapX;
|
|
||||||
secondLastTapY = lastTapY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set current tap as the last tap
|
|
||||||
lastTapTime = touchEndTime;
|
lastTapTime = touchEndTime;
|
||||||
lastTapX = touchX;
|
lastTapX = touchX;
|
||||||
lastTapY = touchY;
|
lastTapY = touchY;
|
||||||
|
@ -2359,7 +2410,29 @@ function handleTouchEnd(event) {
|
||||||
|
|
||||||
// Create touch instructions overlay
|
// Create touch instructions overlay
|
||||||
function createTouchControlButtons() {
|
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
|
// Create touch instructions overlay
|
||||||
const touchInstructions = document.createElement('div');
|
const touchInstructions = document.createElement('div');
|
||||||
|
@ -2370,7 +2443,7 @@ function createTouchControlButtons() {
|
||||||
<p><b>Swipe down:</b> Soft drop</p>
|
<p><b>Swipe down:</b> Soft drop</p>
|
||||||
<p><b>Tap anywhere:</b> Rotate right</p>
|
<p><b>Tap anywhere:</b> Rotate right</p>
|
||||||
<p><b>Double-tap:</b> Hard drop</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);
|
document.body.appendChild(touchInstructions);
|
||||||
|
|
||||||
|
@ -2408,6 +2481,31 @@ function createTouchControlButtons() {
|
||||||
if (scoreContainer) {
|
if (scoreContainer) {
|
||||||
scoreContainer.appendChild(instructionsBtn);
|
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
|
// Set active piece to next piece and create new next piece
|
||||||
|
@ -2496,10 +2594,23 @@ function getNextPieceFromBag() {
|
||||||
const tetromino = PIECES[pieceIndex];
|
const tetromino = PIECES[pieceIndex];
|
||||||
const color = COLORS[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
|
// Random rotation/orientation
|
||||||
const randomIndex = Math.floor(Math.random() * tetromino.length);
|
const randomIndex = Math.floor(Math.random() * tetromino.length);
|
||||||
|
|
||||||
return new Piece(tetromino, randomIndex, color);
|
return new Piece(tetromino, randomIndex, color, tetrominoType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the game
|
// Start the game
|
||||||
|
|
139
style.css
139
style.css
|
@ -249,7 +249,7 @@ canvas {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -260,25 +260,23 @@ canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(10, 10, 20, 0.85);
|
||||||
padding: 30px;
|
border: 2px solid rgba(0, 255, 255, 0.6);
|
||||||
border-radius: 10px;
|
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;
|
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 {
|
.modal h2 {
|
||||||
from {transform: scale(0.8); opacity: 0;}
|
margin-top: 0;
|
||||||
to {transform: scale(1); opacity: 1;}
|
color: #00ffff;
|
||||||
}
|
text-shadow: 0 0 10px rgba(0, 255, 255, 0.7);
|
||||||
|
text-transform: uppercase;
|
||||||
.modal-content h2 {
|
letter-spacing: 2px;
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #ff0066;
|
|
||||||
text-shadow: 0 0 10px #ff0066;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content p {
|
.modal-content p {
|
||||||
|
@ -387,17 +385,32 @@ canvas#tetris {
|
||||||
/* Options menu styles */
|
/* Options menu styles */
|
||||||
.option-row {
|
.option-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 20px 0;
|
margin: 15px 0;
|
||||||
padding: 10px;
|
position: relative;
|
||||||
background: rgba(51, 51, 51, 0.8);
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-row label {
|
.option-row label {
|
||||||
font-size: 14px;
|
margin-right: 10px;
|
||||||
color: #fff;
|
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 */
|
/* Toggle switch styles */
|
||||||
|
@ -497,6 +510,86 @@ input[type=range]::-moz-range-thumb {
|
||||||
touch-action: none;
|
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 {
|
.mobile-mode .game-container {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue