| 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() { |
| |
| this.gameMode = this.registry.get('gameMode') || 'classic'; |
| this.tetrominoes = this.gameMode === 'advanced' ? ADVANCED_TETROMINOES : TETROMINOES; |
|
|
| |
| this.game.canvas.setAttribute('tabindex', '1'); |
| this.game.canvas.focus(); |
| this.game.canvas.style.outline = 'none'; |
|
|
| |
| this.focusWarning = null; |
|
|
| |
| this.game.canvas.addEventListener('click', () => { |
| this.game.canvas.focus(); |
| if (this.focusWarning) { |
| this.focusWarning.destroy(); |
| this.focusWarning = null; |
| } |
| }); |
|
|
| |
| 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; |
| } |
| }); |
|
|
| |
| 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); |
| this.createUI(); |
| this.spawnPiece(); this.nextPiece = this.getRandomPiece(); |
| this.updateNextPieceDisplay(); |
|
|
| |
| 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() { |
| |
| 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; |
|
|
| |
| 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); |
|
|
| |
| this.time.delayedCall(1500, () => { |
| |
| this.tweens.add({ |
| targets: [levelText, titleText], |
| alpha: 0, |
| duration: 300, |
| onComplete: () => { |
| levelText.destroy(); |
| titleText.destroy(); |
| } |
| }); |
|
|
| |
| SoundGenerator.playWoosh(); |
|
|
| |
| if (this.playAreaContainer) { |
| this.tweens.add({ |
| targets: this.playAreaContainer, |
| y: 0, |
| duration: 600, |
| ease: 'Bounce.easeOut' |
| }); |
| } |
|
|
| |
| if (this.uiPanelContainer) { |
| this.tweens.add({ |
| targets: this.uiPanelContainer, |
| y: 0, |
| duration: 600, |
| delay: 100, |
| ease: 'Bounce.easeOut', |
| onComplete: () => { |
| |
| 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) => { |
| |
| 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() { |
| |
| 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); |
|
|
| |
| this.dasDelay = 16; |
| this.dasSpeed = 4; |
| this.leftHoldCounter = 0; |
| this.rightHoldCounter = 0; |
|
|
| |
| this.moveGracePeriod = 2; |
| 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() { |
| |
| 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); |
|
|
| |
| this.uiPanelContainer = this.add.container(0, 0); |
| const panelGraphics = this.add.graphics(); |
|
|
| |
| const frameWidth = UI.PANEL_WIDTH - 3; |
| const x = UI.PANEL_X + UI.PADDING; |
| let y = PLAY_AREA_Y; |
|
|
| |
| 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; |
|
|
| |
| 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; |
|
|
| |
| 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; |
|
|
| |
| const nextFrameHeight = 42; |
| 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; |
|
|
| |
| 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; |
|
|
| |
| 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() { |
| |
| if (Phaser.Input.Keyboard.JustDown(this.cursors.up)) { |
| this.rotatePiece(); |
| } |
|
|
| |
| if (Phaser.Input.Keyboard.JustDown(this.spaceKey)) { |
| this.hardDrop(); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| if (this.leftGraceCounter > 0) this.leftGraceCounter--; |
| if (this.rightGraceCounter > 0) this.rightGraceCounter--; |
|
|
| |
| 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; |
| } |
|
|
| |
| 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); |
|
|
| |
| if (!this.checkCollision(rotated, this.currentX, this.currentY)) { |
| this.currentPiece = rotated; |
| SoundGenerator.playRotate(); |
| return; |
| } |
|
|
| |
| if (!this.checkCollision(rotated, this.currentX + 1, this.currentY)) { |
| this.currentPiece = rotated; |
| this.currentX++; |
| SoundGenerator.playRotate(); |
| return; |
| } |
|
|
| |
| if (!this.checkCollision(rotated, this.currentX - 1, this.currentY)) { |
| this.currentPiece = rotated; |
| this.currentX--; |
| SoundGenerator.playRotate(); |
| return; |
| } |
|
|
| |
| if (!this.checkCollision(rotated, this.currentX + 2, this.currentY)) { |
| this.currentPiece = rotated; |
| this.currentX += 2; |
| SoundGenerator.playRotate(); |
| return; |
| } |
|
|
| |
| if (!this.checkCollision(rotated, this.currentX - 2, this.currentY)) { |
| this.currentPiece = rotated; |
| this.currentX -= 2; |
| SoundGenerator.playRotate(); |
| return; |
| } |
|
|
| |
| } |
|
|
| 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() { |
| |
| 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; |
| } |
|
|
| |
| this.clearing = true; |
|
|
| |
| SoundGenerator.playLineClear(completeLines.length); |
|
|
| |
| this.redrawGrid(); |
|
|
| |
| this.animateLineClear(completeLines); |
| } |
|
|
| animateLineClear(completeLines) { |
| |
| 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; |
|
|
| |
| const colorIndex = blockType - 1; |
| const color = this.colorPalette[colorIndex % this.colorPalette.length]; |
|
|
| |
| 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); |
| } |
|
|
| |
| const sprite = this.add.sprite(px, py, frames[4]).setOrigin(0, 0); |
| sprite.setDepth(50); |
| crushSprites.push({ sprite, frames }); |
| } |
| }); |
|
|
| |
| let frameCounter = 4; |
|
|
| this.time.addEvent({ |
| delay: 75, |
| repeat: 4, |
| callback: () => { |
| if (frameCounter > 0) { |
| frameCounter--; |
| crushSprites.forEach(crushData => { |
| crushData.sprite.setTexture(crushData.frames[frameCounter]); |
| }); |
| } |
| } |
| }); |
|
|
| |
| this.time.delayedCall(350, () => { |
| crushSprites.forEach(crushData => { |
| crushData.sprite.destroy(); |
| }); |
|
|
| |
| texturesToCleanup.forEach(frameKey => { |
| if (this.textures.exists(frameKey)) { |
| this.textures.remove(frameKey); |
| } |
| }); |
|
|
| this.finishLineClear(completeLines); |
| }); |
| } |
|
|
| finishLineClear(completeLines) { |
| |
| 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; |
| } |
|
|
| |
| 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; |
|
|
| |
| |
| this.redrawGrid(); |
|
|
| |
| const sortedLines = [...validLines].sort((a, b) => a - b); |
|
|
| this.blockSprites.forEach(sprite => { |
| const spriteGridY = Math.floor((sprite.y - PLAY_AREA_Y) / BLOCK_SIZE); |
|
|
| |
| let linesBelowCount = 0; |
| sortedLines.forEach(clearedY => { |
| if (clearedY > spriteGridY - validLines.length) { |
| linesBelowCount++; |
| } |
| }); |
|
|
| if (linesBelowCount > 0) { |
| |
| 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' |
| }); |
| } |
| }); |
|
|
| |
| this.time.delayedCall(160, () => { |
| this.finishScoring(validLines); |
| }); |
| } |
|
|
| finishScoring(validLines) { |
| |
| 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; |
| } |
|
|
| |
| const isPerfectClear = this.grid.every(row => row.every(cell => cell === 0)); |
| if (isPerfectClear) { |
| this.score += SCORES.PERFECT_CLEAR * levelMultiplier; |
| |
| 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); |
|
|
| |
| this.tweens.add({ |
| targets: perfectText, |
| scale: 1.5, |
| alpha: 0, |
| duration: 2000, |
| ease: 'Power2', |
| onComplete: () => perfectText.destroy() |
| }); |
|
|
| |
| SoundGenerator.playLevelUp(); |
| } |
|
|
| |
| 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(); |
|
|
| |
| this.showLevelTransition(newLevel); |
| } else { |
| this.updateUI(); |
| this.clearing = false; |
| this.spawnPiece(); |
| } |
| } |
|
|
|
|
|
|
| showLevelTransition(newLevel) { |
| |
| this.clearing = true; |
|
|
| |
| const blackScreen = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000); |
| blackScreen.setDepth(200); |
| blackScreen.setAlpha(0); |
|
|
| |
| this.tweens.add({ |
| targets: blackScreen, |
| alpha: 1, |
| duration: 300, |
| ease: 'Power2', |
| onComplete: () => { |
| |
| const backdropKey = `backdrop-${newLevel}`; |
| const rawPalette = ColorExtractor.extractPalette(this, backdropKey); |
| const newPalette = SpriteBlockRenderer.enhancePalette(rawPalette); |
|
|
| |
| const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 60, `LEVEL ${newLevel}`, 20); |
| levelText.setOrigin(0.5); |
| levelText.setDepth(201); |
| levelText.setAlpha(0); |
|
|
| |
| 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); |
|
|
| |
| const previewBlocks = []; |
| const startX = GAME_WIDTH / 2 + BORDER_OFFSET - 32; |
| const startY = 120; |
|
|
| for (let i = 0; i < 7; i++) { |
| const x = startX + i * 10; |
| const y = startY; |
| const blockKey = `preview-block-${i}`; |
|
|
| |
| 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 }); |
| } |
|
|
| |
| 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: () => { |
| |
| this.time.delayedCall(1200, () => { |
| |
| this.tweens.add({ |
| targets: [levelText, subtitle, ...previewBlocks.map(b => b.sprite)], |
| alpha: 0, |
| duration: 300, |
| onComplete: () => { |
| |
| levelText.destroy(); |
| subtitle.destroy(); |
| previewBlocks.forEach(b => { |
| b.sprite.destroy(); |
| if (this.textures.exists(b.key)) { |
| this.textures.remove(b.key); |
| } |
| }); |
|
|
| |
| this.time.delayedCall(300, () => { |
| |
| this.blockSprites.forEach(sprite => sprite.destroy()); |
| this.blockSprites = []; |
| this.ghostSprites.forEach(sprite => sprite.destroy()); |
| this.ghostSprites = []; |
|
|
| |
| this.loadLevel(newLevel, false); |
| this.updateUI(); |
| this.clearing = false; |
| this.spawnPiece(); |
|
|
| |
| 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; |
|
|
| |
| this.tweens.add({ |
| targets: blackScreen, |
| alpha: 0, |
| duration: 500, |
| ease: 'Power2', |
| onComplete: () => { |
| blackScreen.destroy(); |
| |
| |
| this.time.delayedCall(1000, () => { |
| |
| SoundGenerator.playWoosh(); |
|
|
| |
| if (this.playAreaContainer) { |
| this.tweens.add({ |
| targets: this.playAreaContainer, |
| y: 0, |
| duration: 600, |
| ease: 'Bounce.easeOut' |
| }); |
| } |
|
|
| |
| if (this.uiPanelContainer) { |
| this.tweens.add({ |
| targets: this.uiPanelContainer, |
| y: 0, |
| duration: 600, |
| delay: 100, |
| ease: 'Bounce.easeOut', |
| onComplete: () => { |
| |
| 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); |
| |
| 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(); |
|
|
| |
| 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'); |
| }); |
| } |
| } |
|
|