import Phaser from 'phaser'; import { GAME_WIDTH, GAME_HEIGHT, BLOCK_SIZE, GRID_WIDTH, GRID_HEIGHT, PLAY_AREA_X, PLAY_AREA_Y, PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT, TETROMINOES, ADVANCED_TETROMINOES, SCORES, LEVEL_SPEEDS, MAX_LEVEL, UI, BORDER_OFFSET, LEVEL_TITLES } from '../constants.js'; import ColorExtractor from '../utils/ColorExtractor.js'; import SpriteBlockRenderer from '../utils/SpriteBlockRenderer.js'; import SoundGenerator from '../utils/SoundGenerator.js'; import { CONFIG } from '../config.js'; export default class GameScene extends Phaser.Scene { constructor() { super({ key: 'GameScene' }); } create() { // Get game mode from registry (set by ModeSelectScene) this.gameMode = this.registry.get('gameMode') || 'classic'; this.tetrominoes = this.gameMode === 'advanced' ? ADVANCED_TETROMINOES : TETROMINOES; // CRITICAL: Ensure canvas has focus and can receive keyboard events this.game.canvas.setAttribute('tabindex', '1'); this.game.canvas.focus(); this.game.canvas.style.outline = 'none'; // Visual indicator for focus loss this.focusWarning = null; // Re-focus on any click this.game.canvas.addEventListener('click', () => { this.game.canvas.focus(); if (this.focusWarning) { this.focusWarning.destroy(); this.focusWarning = null; } }); // Monitor focus state this.game.canvas.addEventListener('blur', () => { console.log('Canvas lost focus!'); if (!this.focusWarning) { this.focusWarning = this.add.text(GAME_WIDTH / 2 + BORDER_OFFSET, 10, 'CLICK TO FOCUS', { fontSize: '8px', color: '#ff0000', backgroundColor: '#000000' }).setOrigin(0.5).setDepth(300); } }); this.game.canvas.addEventListener('focus', () => { console.log('Canvas gained focus'); if (this.focusWarning) { this.focusWarning.destroy(); this.focusWarning = null; } }); // Re-focus if window regains focus window.addEventListener('focus', () => { this.game.canvas.focus(); }); this.grid = this.createEmptyGrid(); this.score = 0; this.level = 1; this.lines = 0; this.gameOver = false; this.clearing = false; this.dropCounter = 0; this.dropInterval = LEVEL_SPEEDS[0]; this.softDropping = false; this.softDropCounter = 0; this.inputEnabled = true; this.currentPiece = null; this.nextPiece = null; this.currentX = 0; this.currentY = 0; this.blockSprites = []; this.ghostSprites = []; this.setupInput(); this.loadLevel(this.level, false); // Load level first without intro this.createUI(); // Create UI after level is loaded this.spawnPiece(); this.nextPiece = this.getRandomPiece(); this.updateNextPieceDisplay(); // Show intro animation after everything is set up this.showLevelIntro(); } createEmptyGrid() { const grid = []; for (let y = 0; y < GRID_HEIGHT; y++) { grid[y] = []; for (let x = 0; x < GRID_WIDTH; x++) grid[y][x] = 0; } return grid; } loadLevel(level, showIntro = false) { if (this.currentMusic) this.currentMusic.stop(); const backdropKey = `backdrop-${level}`; if (this.backdrop) this.backdrop.destroy(); this.backdrop = this.add.image(BORDER_OFFSET, 0, backdropKey).setOrigin(0, 0); this.backdrop.setDisplaySize(GAME_WIDTH, GAME_HEIGHT); this.backdrop.setDepth(-1); this.colorPalette = ColorExtractor.extractPalette(this, backdropKey); this.createBlockTextures(); const musicKey = `music-${level}`; this.currentMusic = this.sound.add(musicKey, { loop: true, volume: 0.5 }); this.currentMusic.play(); this.redrawGrid(); if (showIntro) { this.showLevelIntro(); } } showLevelIntro() { // Immediately move containers off-screen (before any delay) if (this.playAreaContainer) { this.playAreaContainer.y = -GAME_HEIGHT; } if (this.uiPanelContainer) { this.uiPanelContainer.y = -GAME_HEIGHT; } // Hide all block sprites (current piece and grid) this.blockSprites.forEach(sprite => sprite.setVisible(false)); this.ghostSprites.forEach(sprite => sprite.setVisible(false)); // Disable input temporarily this.inputEnabled = false; // Show level title text const levelTitle = LEVEL_TITLES[this.level] || 'Unknown'; const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 - 10, `LEVEL ${this.level}`, 16); levelText.setOrigin(0.5); levelText.setDepth(201); const titleText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 10, levelTitle, 10); titleText.setOrigin(0.5); titleText.setDepth(201); // Wait 1 second showing backdrop and title this.time.delayedCall(1500, () => { // Fade out title texts this.tweens.add({ targets: [levelText, titleText], alpha: 0, duration: 300, onComplete: () => { levelText.destroy(); titleText.destroy(); } }); // Play woosh sound SoundGenerator.playWoosh(); // Animate play area falling in if (this.playAreaContainer) { this.tweens.add({ targets: this.playAreaContainer, y: 0, duration: 600, ease: 'Bounce.easeOut' }); } // Animate UI panel falling in (slightly delayed) if (this.uiPanelContainer) { this.tweens.add({ targets: this.uiPanelContainer, y: 0, duration: 600, delay: 100, ease: 'Bounce.easeOut', onComplete: () => { // Show blocks and re-enable input after animations complete this.blockSprites.forEach(sprite => sprite.setVisible(true)); this.ghostSprites.forEach(sprite => sprite.setVisible(true)); this.inputEnabled = true; } }); } }); } createBlockTextures() { const enhanced = SpriteBlockRenderer.enhancePalette(this.colorPalette); this.colorPalette = enhanced; Object.keys(this.tetrominoes).forEach((key, i) => { // Remove old textures if they exist if (this.textures.exists(`block-${key}`)) { this.textures.remove(`block-${key}`); } if (this.textures.exists(`ghost-${key}`)) { this.textures.remove(`ghost-${key}`); } SpriteBlockRenderer.createBlockTexture(this, this.colorPalette, this.level, `block-${key}`, i); SpriteBlockRenderer.createBlockTexture(this, this.colorPalette, this.level, `ghost-${key}`, i); }); } setupInput() { // Simple polling - use Phaser's built-in JustDown this.cursors = this.input.keyboard.createCursorKeys(); this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); this.pKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.P); // DAS settings for left/right auto-repeat when HOLDING this.dasDelay = 16; // Frames before repeat starts (longer delay) this.dasSpeed = 4; // Frames between repeats (slower repeat) this.leftHoldCounter = 0; this.rightHoldCounter = 0; // Grace period to prevent double-taps this.moveGracePeriod = 2; // Minimum frames between moves this.leftGraceCounter = 0; this.rightGraceCounter = 0; this.paused = false; } createBitmapText(x, y, text, size = 10) { const t = this.add.bitmapText(x, y, 'pixel-font', text.toUpperCase(), size); t.texture.setFilter(Phaser.Textures.FilterMode.NEAREST); return t; } createUI() { // Create container for play area (so it can be animated as a unit) this.playAreaContainer = this.add.container(0, 0); const playAreaGraphics = this.add.graphics(); this.drawNESFrame(playAreaGraphics, PLAY_AREA_X - 2, PLAY_AREA_Y - 2, PLAY_AREA_WIDTH + 5, PLAY_AREA_HEIGHT + 4); this.playAreaContainer.add(playAreaGraphics); // Create container for right-side UI panels this.uiPanelContainer = this.add.container(0, 0); const panelGraphics = this.add.graphics(); // UI text positions - align first frame with play area top const frameWidth = UI.PANEL_WIDTH - 3; const x = UI.PANEL_X + UI.PADDING; let y = PLAY_AREA_Y; // Align with play area top // SCORE frame this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26); const scoreLabel = this.createBitmapText(x, y + 2, 'SCORE'); y += 12; this.scoreText = this.createBitmapText(x, y + 2, '000000'); y += 12 + 12; // 12px vertical space // LEVEL frame this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26); const levelLabel = this.createBitmapText(x, y + 2, 'LEVEL'); y += 12; this.levelText = this.createBitmapText(x, y + 2, '1'); y += 12 + 12; // 12px vertical space // LINES frame this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26); const linesLabel = this.createBitmapText(x, y + 2, 'LINES'); y += 12; this.linesText = this.createBitmapText(x, y + 2, '0'); y += 12 + 12; // 12px vertical space // NEXT frame const nextFrameHeight = 42; // Enough for piece preview + 2px top padding this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, nextFrameHeight); this.nextPieceText = this.createBitmapText(x, y + 2, 'NEXT'); this.nextPieceY = y + 16; this.nextPieceX = x; // Add all UI elements to the container this.uiPanelContainer.add([ panelGraphics, scoreLabel, this.scoreText, levelLabel, this.levelText, linesLabel, this.linesText, this.nextPieceText ]); } drawNESFrame(g, x, y, w, h) { g.fillStyle(0x000000, 1); g.fillRect(x, y, w, h); g.lineStyle(2, 0xAAAAAA, 1); g.strokeRect(x, y, w, h); g.lineStyle(1, 0x555555, 1); g.strokeRect(x + 2, y + 2, w - 4, h - 4); g.lineStyle(1, 0xFFFFFF, 1); g.beginPath(); g.moveTo(x + 1, y + h - 1); g.lineTo(x + 1, y + 1); g.lineTo(x + w - 1, y + 1); g.strokePath(); g.lineStyle(1, 0x333333, 1); g.beginPath(); g.moveTo(x + w - 1, y + 1); g.lineTo(x + w - 1, y + h - 1); g.lineTo(x + 1, y + h - 1); g.strokePath(); } getRandomPiece() { const keys = Object.keys(this.tetrominoes); return JSON.parse(JSON.stringify(this.tetrominoes[keys[Math.floor(Math.random() * keys.length)]])); } spawnPiece() { this.currentPiece = this.nextPiece ? this.nextPiece : this.getRandomPiece(); this.nextPiece = this.getRandomPiece(); this.currentX = Math.floor(GRID_WIDTH / 2) - Math.floor(this.currentPiece.shape[0].length / 2); this.currentY = 0; if (this.checkCollision(this.currentPiece, this.currentX, this.currentY)) { this.gameOver = true; this.handleGameOver(); } this.updateNextPieceDisplay(); } update(time, delta) { if (this.gameOver || !this.inputEnabled) return; // Pause check - always available if (Phaser.Input.Keyboard.JustDown(this.pKey)) { this.togglePause(); } if (this.clearing || this.paused) return; this.handleInput(); this.dropCounter++; if (this.dropCounter >= this.dropInterval) { this.dropCounter = 0; this.moveDown(); } this.renderPiece(); } togglePause() { this.paused = !this.paused; if (this.paused) { this.pauseOverlay = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.8); this.pauseOverlay.setDepth(100); this.pauseText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, 'PAUSED'); this.pauseText.setOrigin(0.5).setDepth(101); this.pauseHintText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 12, 'PRESS P'); this.pauseHintText.setOrigin(0.5).setDepth(101); if (this.currentMusic) this.currentMusic.pause(); } else { if (this.pauseOverlay) { this.pauseOverlay.destroy(); this.pauseOverlay = null; } if (this.pauseText) { this.pauseText.destroy(); this.pauseText = null; } if (this.pauseHintText) { this.pauseHintText.destroy(); this.pauseHintText = null; } if (this.currentMusic) this.currentMusic.resume(); } } handleInput() { // Rotation - JustDown ensures one action per press if (Phaser.Input.Keyboard.JustDown(this.cursors.up)) { this.rotatePiece(); } // Hard drop - JustDown ensures one action per press if (Phaser.Input.Keyboard.JustDown(this.spaceKey)) { this.hardDrop(); } // Soft drop (down key held) - continuous action if (this.cursors.down.isDown) { if (!this.softDropping) { this.softDropping = true; this.softDropCounter = 0; } this.softDropCounter++; if (this.softDropCounter >= 2) { this.softDropCounter = 0; if (this.moveDown()) { this.score += SCORES.SOFT_DROP; this.updateUI(); SoundGenerator.playSoftDrop(); } } } else { this.softDropping = false; this.softDropCounter = 0; } // Decrement grace counters if (this.leftGraceCounter > 0) this.leftGraceCounter--; if (this.rightGraceCounter > 0) this.rightGraceCounter--; // LEFT - JustDown for first press, then auto-repeat when held if (Phaser.Input.Keyboard.JustDown(this.cursors.left) && this.leftGraceCounter === 0) { this.moveLeft(); this.leftHoldCounter = 0; this.leftGraceCounter = this.moveGracePeriod; } else if (this.cursors.left.isDown && this.leftGraceCounter === 0) { this.leftHoldCounter++; if (this.leftHoldCounter >= this.dasDelay && (this.leftHoldCounter - this.dasDelay) % this.dasSpeed === 0) { this.moveLeft(); this.leftGraceCounter = this.moveGracePeriod; } } else if (!this.cursors.left.isDown) { this.leftHoldCounter = 0; } // RIGHT - JustDown for first press, then auto-repeat when held if (Phaser.Input.Keyboard.JustDown(this.cursors.right) && this.rightGraceCounter === 0) { this.moveRight(); this.rightHoldCounter = 0; this.rightGraceCounter = this.moveGracePeriod; } else if (this.cursors.right.isDown && this.rightGraceCounter === 0) { this.rightHoldCounter++; if (this.rightHoldCounter >= this.dasDelay && (this.rightHoldCounter - this.dasDelay) % this.dasSpeed === 0) { this.moveRight(); this.rightGraceCounter = this.moveGracePeriod; } } else if (!this.cursors.right.isDown) { this.rightHoldCounter = 0; } } moveLeft() { if (!this.checkCollision(this.currentPiece, this.currentX - 1, this.currentY)) { this.currentX--; SoundGenerator.playMove(); } } moveRight() { if (!this.checkCollision(this.currentPiece, this.currentX + 1, this.currentY)) { this.currentX++; SoundGenerator.playMove(); } } moveDown() { if (!this.checkCollision(this.currentPiece, this.currentX, this.currentY + 1)) { this.currentY++; return true; } else { this.lockPiece(); return false; } } hardDrop() { while (!this.checkCollision(this.currentPiece, this.currentX, this.currentY + 1)) this.currentY++; SoundGenerator.playDrop(); this.lockPiece(); } rotatePiece() { const rotated = this.getRotatedPiece(this.currentPiece); // Try rotation at current position if (!this.checkCollision(rotated, this.currentX, this.currentY)) { this.currentPiece = rotated; SoundGenerator.playRotate(); return; } // Wall kick: try shifting right if (!this.checkCollision(rotated, this.currentX + 1, this.currentY)) { this.currentPiece = rotated; this.currentX++; SoundGenerator.playRotate(); return; } // Wall kick: try shifting left if (!this.checkCollision(rotated, this.currentX - 1, this.currentY)) { this.currentPiece = rotated; this.currentX--; SoundGenerator.playRotate(); return; } // Wall kick: try shifting right 2 spaces (for I-piece) if (!this.checkCollision(rotated, this.currentX + 2, this.currentY)) { this.currentPiece = rotated; this.currentX += 2; SoundGenerator.playRotate(); return; } // Wall kick: try shifting left 2 spaces (for I-piece) if (!this.checkCollision(rotated, this.currentX - 2, this.currentY)) { this.currentPiece = rotated; this.currentX -= 2; SoundGenerator.playRotate(); return; } // Rotation failed - no valid position found } getRotatedPiece(piece) { const rotated = JSON.parse(JSON.stringify(piece)); const shape = piece.shape; const rows = shape.length; const cols = shape[0].length; const newShape = []; for (let x = 0; x < cols; x++) { newShape[x] = []; for (let y = rows - 1; y >= 0; y--) newShape[x][rows - 1 - y] = shape[y][x]; } rotated.shape = newShape; return rotated; } checkCollision(piece, x, y) { const shape = piece.shape; for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const gridX = x + col; const gridY = y + row; if (gridX < 0 || gridX >= GRID_WIDTH || gridY >= GRID_HEIGHT) return true; if (gridY >= 0 && this.grid[gridY][gridX]) return true; } } } return false; } lockPiece() { const shape = this.currentPiece.shape; for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const gridX = this.currentX + col; const gridY = this.currentY + row; if (gridY >= 0) this.grid[gridY][gridX] = this.currentPiece.name; } } } this.checkAndClearLines(); } checkAndClearLines() { // Find complete lines - a line is complete ONLY if every cell is filled const completeLines = []; for (let y = 0; y < GRID_HEIGHT; y++) { let isComplete = true; for (let x = 0; x < GRID_WIDTH; x++) { if (!this.grid[y][x]) { isComplete = false; break; } } if (isComplete) { console.log(`Line ${y} is complete:`, JSON.stringify(this.grid[y])); completeLines.push(y); } } if (completeLines.length > 0) { console.log('Complete lines found:', completeLines); console.log('Grid state:', JSON.stringify(this.grid)); } if (completeLines.length === 0) { this.spawnPiece(); this.redrawGrid(); return; } // Block game updates during line clear this.clearing = true; // Play sound based on number of lines cleared SoundGenerator.playLineClear(completeLines.length); // Show the locked piece first this.redrawGrid(); // Run the line clear animation, then apply changes this.animateLineClear(completeLines); } animateLineClear(completeLines) { // Create crush animation for each block const crushSprites = []; const texturesToCleanup = []; completeLines.forEach(y => { for (let x = 0; x < GRID_WIDTH; x++) { const blockType = this.grid[y][x]; if (!blockType) continue; const px = PLAY_AREA_X + x * BLOCK_SIZE; const py = PLAY_AREA_Y + y * BLOCK_SIZE; // Get the block's color from the palette const colorIndex = blockType - 1; const color = this.colorPalette[colorIndex % this.colorPalette.length]; // Create unique crush animation frames for this specific block instance const uniqueId = `${Date.now()}-${x}-${y}-${Math.random().toString(36).substr(2, 9)}`; const frames = []; for (let f = 0; f < 5; f++) { const frameKey = `crush-${uniqueId}-${f}`; SpriteBlockRenderer.createCrushTexture(this, color, f, frameKey); frames.push(frameKey); texturesToCleanup.push(frameKey); } // Create sprite starting with frame 4 (most intact) const sprite = this.add.sprite(px, py, frames[4]).setOrigin(0, 0); sprite.setDepth(50); crushSprites.push({ sprite, frames }); } }); // Cycle through the 5 crush frames in REVERSE: 4 -> 3 -> 2 -> 1 -> 0 let frameCounter = 4; this.time.addEvent({ delay: 75, // 75ms per frame (twice as fast) repeat: 4, // repeat 4 times = 5 total callbacks (frames 4,3,2,1,0) callback: () => { if (frameCounter > 0) { frameCounter--; crushSprites.forEach(crushData => { crushData.sprite.setTexture(crushData.frames[frameCounter]); }); } } }); // After all 5 frames (4 shows immediately, then 3,2,1,0 at 75ms each = 300ms total), clean up this.time.delayedCall(350, () => { crushSprites.forEach(crushData => { crushData.sprite.destroy(); }); // Clean up all textures texturesToCleanup.forEach(frameKey => { if (this.textures.exists(frameKey)) { this.textures.remove(frameKey); } }); this.finishLineClear(completeLines); }); } finishLineClear(completeLines) { // Apply the grid changes first const validLines = completeLines.filter(y => { if (y < 0 || y >= GRID_HEIGHT) return false; for (let x = 0; x < GRID_WIDTH; x++) { if (!this.grid[y][x]) return false; } return true; }); if (validLines.length === 0) { console.warn('No valid lines to clear after validation'); this.clearing = false; this.spawnPiece(); this.redrawGrid(); return; } // Build new grid const newGrid = []; const linesToRemove = new Set(validLines); for (let i = 0; i < validLines.length; i++) { newGrid.push(new Array(GRID_WIDTH).fill(0)); } for (let y = 0; y < GRID_HEIGHT; y++) { if (!linesToRemove.has(y)) { newGrid.push([...this.grid[y]]); } } this.grid = newGrid; // Now animate the falling blocks // Rebuild sprites from new grid state this.redrawGrid(); // Animate all sprites falling into place const sortedLines = [...validLines].sort((a, b) => a - b); this.blockSprites.forEach(sprite => { const spriteGridY = Math.floor((sprite.y - PLAY_AREA_Y) / BLOCK_SIZE); // Count how many cleared lines were below this sprite's ORIGINAL position let linesBelowCount = 0; sortedLines.forEach(clearedY => { if (clearedY > spriteGridY - validLines.length) { linesBelowCount++; } }); if (linesBelowCount > 0) { // Start sprite higher, then animate down to current position const startY = sprite.y - (linesBelowCount * BLOCK_SIZE); sprite.y = startY; this.tweens.add({ targets: sprite, y: sprite.y + (linesBelowCount * BLOCK_SIZE), duration: 150, ease: 'Bounce.easeOut' }); } }); // Wait for fall animation, then finish this.time.delayedCall(160, () => { this.finishScoring(validLines); }); } finishScoring(validLines) { // Update score this.lines += validLines.length; const levelMultiplier = this.level; switch (validLines.length) { case 1: this.score += SCORES.SINGLE * levelMultiplier; break; case 2: this.score += SCORES.DOUBLE * levelMultiplier; break; case 3: this.score += SCORES.TRIPLE * levelMultiplier; break; case 4: this.score += SCORES.TETRIS * levelMultiplier; break; } // Check for perfect clear (entire grid is empty) const isPerfectClear = this.grid.every(row => row.every(cell => cell === 0)); if (isPerfectClear) { this.score += SCORES.PERFECT_CLEAR * levelMultiplier; // Show perfect clear message const perfectText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, 'PERFECT CLEAR!', 12); perfectText.setOrigin(0.5); perfectText.setDepth(150); perfectText.setTint(0xFFD700); // Gold color // Animate the text this.tweens.add({ targets: perfectText, scale: 1.5, alpha: 0, duration: 2000, ease: 'Power2', onComplete: () => perfectText.destroy() }); // Play special sound SoundGenerator.playLevelUp(); } // Check for level up const newLevel = Math.min(MAX_LEVEL, Math.floor(this.lines / CONFIG.LINES_PER_LEVEL) + 1); if (newLevel > this.level) { this.level = newLevel; this.dropInterval = LEVEL_SPEEDS[this.level - 1]; SoundGenerator.playLevelUp(); // Exciting level transition! this.showLevelTransition(newLevel); } else { this.updateUI(); this.clearing = false; this.spawnPiece(); } } showLevelTransition(newLevel) { // Keep game paused during transition this.clearing = true; // Black screen overlay const blackScreen = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000); blackScreen.setDepth(200); blackScreen.setAlpha(0); // Fade to black this.tweens.add({ targets: blackScreen, alpha: 1, duration: 300, ease: 'Power2', onComplete: () => { // Pre-load the new level's palette and create preview blocks const backdropKey = `backdrop-${newLevel}`; const rawPalette = ColorExtractor.extractPalette(this, backdropKey); const newPalette = SpriteBlockRenderer.enhancePalette(rawPalette); // Level up text const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 60, `LEVEL ${newLevel}`, 20); levelText.setOrigin(0.5); levelText.setDepth(201); levelText.setAlpha(0); // Subtitle - show level title const levelTitle = LEVEL_TITLES[newLevel] || 'Unknown'; const subtitle = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 85, levelTitle, 10); subtitle.setOrigin(0.5); subtitle.setDepth(201); subtitle.setAlpha(0); // Create preview blocks showing new level's style const previewBlocks = []; const startX = GAME_WIDTH / 2 + BORDER_OFFSET - 32; // Center 8 blocks (8*8 = 64px wide) const startY = 120; for (let i = 0; i < 7; i++) { const x = startX + i * 10; const y = startY; const blockKey = `preview-block-${i}`; // Create block texture with new level's palette SpriteBlockRenderer.createBlockTexture(this, newPalette, newLevel, blockKey, i); const block = this.add.sprite(x, y, blockKey).setOrigin(0, 0); block.setDepth(201); block.setAlpha(0); block.setScale(0.5); previewBlocks.push({ sprite: block, key: blockKey }); } // Animate text and blocks in this.tweens.add({ targets: [levelText, subtitle], alpha: 1, duration: 400, ease: 'Power2' }); this.tweens.add({ targets: previewBlocks.map(b => b.sprite), alpha: 1, scale: 1, duration: 500, delay: 200, ease: 'Back.easeOut', onComplete: () => { // Hold for a moment this.time.delayedCall(1200, () => { // Fade out text and preview blocks only (keep black screen) this.tweens.add({ targets: [levelText, subtitle, ...previewBlocks.map(b => b.sprite)], alpha: 0, duration: 300, onComplete: () => { // Clean up text and preview blocks levelText.destroy(); subtitle.destroy(); previewBlocks.forEach(b => { b.sprite.destroy(); if (this.textures.exists(b.key)) { this.textures.remove(b.key); } }); // Black screen stays for a moment this.time.delayedCall(300, () => { // Destroy old level elements while screen is black this.blockSprites.forEach(sprite => sprite.destroy()); this.blockSprites = []; this.ghostSprites.forEach(sprite => sprite.destroy()); this.ghostSprites = []; // Load new level (no intro yet) this.loadLevel(newLevel, false); this.updateUI(); this.clearing = false; this.spawnPiece(); // IMMEDIATELY hide UI containers before fading out black screen if (this.playAreaContainer) { this.playAreaContainer.y = -GAME_HEIGHT; } if (this.uiPanelContainer) { this.uiPanelContainer.y = -GAME_HEIGHT; } this.blockSprites.forEach(sprite => sprite.setVisible(false)); this.ghostSprites.forEach(sprite => sprite.setVisible(false)); this.inputEnabled = false; // Fade out black screen to reveal ONLY the backdrop this.tweens.add({ targets: blackScreen, alpha: 0, duration: 500, ease: 'Power2', onComplete: () => { blackScreen.destroy(); // Now show the intro animation (UI falls in) // Wait 1 second showing just the backdrop this.time.delayedCall(1000, () => { // Play woosh sound SoundGenerator.playWoosh(); // Animate play area falling in if (this.playAreaContainer) { this.tweens.add({ targets: this.playAreaContainer, y: 0, duration: 600, ease: 'Bounce.easeOut' }); } // Animate UI panel falling in (slightly delayed) if (this.uiPanelContainer) { this.tweens.add({ targets: this.uiPanelContainer, y: 0, duration: 600, delay: 100, ease: 'Bounce.easeOut', onComplete: () => { // Show blocks and re-enable input after animations complete this.blockSprites.forEach(sprite => sprite.setVisible(true)); this.ghostSprites.forEach(sprite => sprite.setVisible(true)); this.inputEnabled = true; } }); } }); } }); }); } }); }); } }); } }); } redrawGrid() { this.blockSprites.forEach(sprite => sprite.destroy()); this.blockSprites = []; for (let y = 0; y < GRID_HEIGHT; y++) { for (let x = 0; x < GRID_WIDTH; x++) { if (this.grid[y][x]) { const blockType = this.grid[y][x]; const sprite = this.add.sprite(PLAY_AREA_X + x * BLOCK_SIZE, PLAY_AREA_Y + y * BLOCK_SIZE, `block-${blockType}`).setOrigin(0, 0); sprite.setDepth(2); this.blockSprites.push(sprite); } } } } renderPiece() { this.blockSprites.forEach(sprite => { if (sprite.getData('current')) sprite.destroy(); }); this.blockSprites = this.blockSprites.filter(s => !s.getData('current')); this.ghostSprites.forEach(sprite => sprite.destroy()); this.ghostSprites = []; if (!this.currentPiece) return; if (this.level === 1) { let ghostY = this.currentY; while (!this.checkCollision(this.currentPiece, this.currentX, ghostY + 1)) ghostY++; const shape = this.currentPiece.shape; for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const x = PLAY_AREA_X + (this.currentX + col) * BLOCK_SIZE; const y = PLAY_AREA_Y + (ghostY + row) * BLOCK_SIZE; const sprite = this.add.sprite(x, y, `block-${this.currentPiece.name}`).setOrigin(0, 0); sprite.setAlpha(0.3); sprite.setDepth(1); this.ghostSprites.push(sprite); } } } } const shape = this.currentPiece.shape; for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const x = PLAY_AREA_X + (this.currentX + col) * BLOCK_SIZE; const y = PLAY_AREA_Y + (this.currentY + row) * BLOCK_SIZE; const sprite = this.add.sprite(x, y, `block-${this.currentPiece.name}`).setOrigin(0, 0); sprite.setData('current', true); sprite.setDepth(2); this.blockSprites.push(sprite); } } } } updateNextPieceDisplay() { if (this.nextPieceSprites) this.nextPieceSprites.forEach(sprite => sprite.destroy()); this.nextPieceSprites = []; if (!this.nextPiece) return; const shape = this.nextPiece.shape; const startX = this.nextPieceX; const startY = this.nextPieceY; for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const x = startX + col * BLOCK_SIZE; const y = startY + row * BLOCK_SIZE; const sprite = this.add.sprite(x, y, `block-${this.nextPiece.name}`).setOrigin(0, 0); sprite.setDepth(20); this.nextPieceSprites.push(sprite); // Add to UI container so it animates with the panel if (this.uiPanelContainer) { this.uiPanelContainer.add(sprite); } } } } } updateUI() { const scoreStr = this.score.toString().padStart(6, '0'); this.scoreText.setText(scoreStr); this.levelText.setText(this.level.toString()); this.linesText.setText(this.lines.toString()); } handleGameOver() { if (this.currentMusic) this.currentMusic.stop(); SoundGenerator.playGameOver(); // Display game over image (256x224, fills the game area) const gameOverImage = this.add.image(BORDER_OFFSET, 0, 'game-over'); gameOverImage.setOrigin(0, 0); gameOverImage.setDisplaySize(GAME_WIDTH, GAME_HEIGHT); gameOverImage.setDepth(100); gameOverImage.texture.setFilter(Phaser.Textures.FilterMode.NEAREST); this.input.keyboard.once('keydown-SPACE', () => { this.scene.start('PreloadScene'); }); } }