Team Ai
Apppublic

broadfield-dev/stack-overflow-game

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
GameScene.js984 linesDownload Raw Back to scenes
1import Phaser from 'phaser';2import {3  GAME_WIDTH, GAME_HEIGHT, BLOCK_SIZE, GRID_WIDTH, GRID_HEIGHT,4  PLAY_AREA_X, PLAY_AREA_Y, PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT,5  TETROMINOES, ADVANCED_TETROMINOES, SCORES, LEVEL_SPEEDS, MAX_LEVEL, UI, BORDER_OFFSET, LEVEL_TITLES6} from '../constants.js';7import ColorExtractor from '../utils/ColorExtractor.js';8import SpriteBlockRenderer from '../utils/SpriteBlockRenderer.js';9import SoundGenerator from '../utils/SoundGenerator.js';10import { CONFIG } from '../config.js';11 12export default class GameScene extends Phaser.Scene {13  constructor() { super({ key: 'GameScene' }); }14 15  create() {16    // Get game mode from registry (set by ModeSelectScene)17    this.gameMode = this.registry.get('gameMode') || 'classic';18    this.tetrominoes = this.gameMode === 'advanced' ? ADVANCED_TETROMINOES : TETROMINOES;19 20    // CRITICAL: Ensure canvas has focus and can receive keyboard events21    this.game.canvas.setAttribute('tabindex', '1');22    this.game.canvas.focus();23    this.game.canvas.style.outline = 'none';24 25    // Visual indicator for focus loss26    this.focusWarning = null;27 28    // Re-focus on any click29    this.game.canvas.addEventListener('click', () => {30      this.game.canvas.focus();31      if (this.focusWarning) {32        this.focusWarning.destroy();33        this.focusWarning = null;34      }35    });36 37    // Monitor focus state38    this.game.canvas.addEventListener('blur', () => {39      console.log('Canvas lost focus!');40      if (!this.focusWarning) {41        this.focusWarning = this.add.text(GAME_WIDTH / 2 + BORDER_OFFSET, 10, 'CLICK TO FOCUS', {42          fontSize: '8px',43          color: '#ff0000',44          backgroundColor: '#000000'45        }).setOrigin(0.5).setDepth(300);46      }47    });48 49    this.game.canvas.addEventListener('focus', () => {50      console.log('Canvas gained focus');51      if (this.focusWarning) {52        this.focusWarning.destroy();53        this.focusWarning = null;54      }55    });56 57    // Re-focus if window regains focus58    window.addEventListener('focus', () => {59      this.game.canvas.focus();60    });61 62    this.grid = this.createEmptyGrid();63    this.score = 0; this.level = 1; this.lines = 0; this.gameOver = false;64    this.clearing = false;65    this.dropCounter = 0; this.dropInterval = LEVEL_SPEEDS[0];66    this.softDropping = false; this.softDropCounter = 0;67    this.inputEnabled = true;68    this.currentPiece = null; this.nextPiece = null;69    this.currentX = 0; this.currentY = 0;70    this.blockSprites = []; this.ghostSprites = [];71    this.setupInput();72    this.loadLevel(this.level, false); // Load level first without intro73    this.createUI(); // Create UI after level is loaded74    this.spawnPiece(); this.nextPiece = this.getRandomPiece();75    this.updateNextPieceDisplay();76 77    // Show intro animation after everything is set up78    this.showLevelIntro();79  }80 81  createEmptyGrid() {82    const grid = [];83    for (let y = 0; y < GRID_HEIGHT; y++) { grid[y] = []; for (let x = 0; x < GRID_WIDTH; x++) grid[y][x] = 0; }84    return grid;85  }86 87  loadLevel(level, showIntro = false) {88    if (this.currentMusic) this.currentMusic.stop();89    const backdropKey = `backdrop-${level}`;90    if (this.backdrop) this.backdrop.destroy();91    this.backdrop = this.add.image(BORDER_OFFSET, 0, backdropKey).setOrigin(0, 0);92    this.backdrop.setDisplaySize(GAME_WIDTH, GAME_HEIGHT);93    this.backdrop.setDepth(-1);94    this.colorPalette = ColorExtractor.extractPalette(this, backdropKey);95    this.createBlockTextures();96    const musicKey = `music-${level}`;97    this.currentMusic = this.sound.add(musicKey, { loop: true, volume: 0.5 });98    this.currentMusic.play();99    this.redrawGrid();100 101    if (showIntro) {102      this.showLevelIntro();103    }104  }105 106  showLevelIntro() {107    // Immediately move containers off-screen (before any delay)108    if (this.playAreaContainer) {109      this.playAreaContainer.y = -GAME_HEIGHT;110    }111    if (this.uiPanelContainer) {112      this.uiPanelContainer.y = -GAME_HEIGHT;113    }114 115    // Hide all block sprites (current piece and grid)116    this.blockSprites.forEach(sprite => sprite.setVisible(false));117    this.ghostSprites.forEach(sprite => sprite.setVisible(false));118 119    // Disable input temporarily120    this.inputEnabled = false;121 122    // Show level title text123    const levelTitle = LEVEL_TITLES[this.level] || 'Unknown';124    const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 - 10, `LEVEL ${this.level}`, 16);125    levelText.setOrigin(0.5);126    levelText.setDepth(201);127 128    const titleText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 10, levelTitle, 10);129    titleText.setOrigin(0.5);130    titleText.setDepth(201);131 132    // Wait 1 second showing backdrop and title133    this.time.delayedCall(1500, () => {134      // Fade out title texts135      this.tweens.add({136        targets: [levelText, titleText],137        alpha: 0,138        duration: 300,139        onComplete: () => {140          levelText.destroy();141          titleText.destroy();142        }143      });144 145      // Play woosh sound146      SoundGenerator.playWoosh();147 148      // Animate play area falling in149      if (this.playAreaContainer) {150        this.tweens.add({151          targets: this.playAreaContainer,152          y: 0,153          duration: 600,154          ease: 'Bounce.easeOut'155        });156      }157 158      // Animate UI panel falling in (slightly delayed)159      if (this.uiPanelContainer) {160        this.tweens.add({161          targets: this.uiPanelContainer,162          y: 0,163          duration: 600,164          delay: 100,165          ease: 'Bounce.easeOut',166          onComplete: () => {167            // Show blocks and re-enable input after animations complete168            this.blockSprites.forEach(sprite => sprite.setVisible(true));169            this.ghostSprites.forEach(sprite => sprite.setVisible(true));170            this.inputEnabled = true;171          }172        });173      }174    });175  }176 177  createBlockTextures() {178    const enhanced = SpriteBlockRenderer.enhancePalette(this.colorPalette);179    this.colorPalette = enhanced;180    Object.keys(this.tetrominoes).forEach((key, i) => {181      // Remove old textures if they exist182      if (this.textures.exists(`block-${key}`)) {183        this.textures.remove(`block-${key}`);184      }185      if (this.textures.exists(`ghost-${key}`)) {186        this.textures.remove(`ghost-${key}`);187      }188      SpriteBlockRenderer.createBlockTexture(this, this.colorPalette, this.level, `block-${key}`, i);189      SpriteBlockRenderer.createBlockTexture(this, this.colorPalette, this.level, `ghost-${key}`, i);190    });191  }192 193  setupInput() {194    // Simple polling - use Phaser's built-in JustDown195    this.cursors = this.input.keyboard.createCursorKeys();196    this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);197    this.pKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.P);198 199    // DAS settings for left/right auto-repeat when HOLDING200    this.dasDelay = 16;  // Frames before repeat starts (longer delay)201    this.dasSpeed = 4;   // Frames between repeats (slower repeat)202    this.leftHoldCounter = 0;203    this.rightHoldCounter = 0;204 205    // Grace period to prevent double-taps206    this.moveGracePeriod = 2; // Minimum frames between moves207    this.leftGraceCounter = 0;208    this.rightGraceCounter = 0;209 210    this.paused = false;211  }212 213  createBitmapText(x, y, text, size = 10) {214    const t = this.add.bitmapText(x, y, 'pixel-font', text.toUpperCase(), size);215    t.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);216    return t;217  }218 219  createUI() {220    // Create container for play area (so it can be animated as a unit)221    this.playAreaContainer = this.add.container(0, 0);222    const playAreaGraphics = this.add.graphics();223    this.drawNESFrame(playAreaGraphics, PLAY_AREA_X - 2, PLAY_AREA_Y - 2, PLAY_AREA_WIDTH + 5, PLAY_AREA_HEIGHT + 4);224    this.playAreaContainer.add(playAreaGraphics);225 226    // Create container for right-side UI panels227    this.uiPanelContainer = this.add.container(0, 0);228    const panelGraphics = this.add.graphics();229 230    // UI text positions - align first frame with play area top231    const frameWidth = UI.PANEL_WIDTH - 3;232    const x = UI.PANEL_X + UI.PADDING;233    let y = PLAY_AREA_Y; // Align with play area top234 235    // SCORE frame236    this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26);237    const scoreLabel = this.createBitmapText(x, y + 2, 'SCORE');238    y += 12;239    this.scoreText = this.createBitmapText(x, y + 2, '000000');240    y += 12 + 12; // 12px vertical space241 242    // LEVEL frame243    this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26);244    const levelLabel = this.createBitmapText(x, y + 2, 'LEVEL');245    y += 12;246    this.levelText = this.createBitmapText(x, y + 2, '1');247    y += 12 + 12; // 12px vertical space248 249    // LINES frame250    this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26);251    const linesLabel = this.createBitmapText(x, y + 2, 'LINES');252    y += 12;253    this.linesText = this.createBitmapText(x, y + 2, '0');254    y += 12 + 12; // 12px vertical space255 256    // NEXT frame257    const nextFrameHeight = 42; // Enough for piece preview + 2px top padding258    this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, nextFrameHeight);259    this.nextPieceText = this.createBitmapText(x, y + 2, 'NEXT');260    this.nextPieceY = y + 16;261    this.nextPieceX = x;262 263    // Add all UI elements to the container264    this.uiPanelContainer.add([265      panelGraphics,266      scoreLabel,267      this.scoreText,268      levelLabel,269      this.levelText,270      linesLabel,271      this.linesText,272      this.nextPieceText273    ]);274  }275 276  drawNESFrame(g, x, y, w, h) {277    g.fillStyle(0x000000, 1); g.fillRect(x, y, w, h);278    g.lineStyle(2, 0xAAAAAA, 1); g.strokeRect(x, y, w, h);279    g.lineStyle(1, 0x555555, 1); g.strokeRect(x + 2, y + 2, w - 4, h - 4);280    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();281    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();282  }283 284  getRandomPiece() {285    const keys = Object.keys(this.tetrominoes);286    return JSON.parse(JSON.stringify(this.tetrominoes[keys[Math.floor(Math.random() * keys.length)]]));287  }288 289  spawnPiece() {290    this.currentPiece = this.nextPiece ? this.nextPiece : this.getRandomPiece();291    this.nextPiece = this.getRandomPiece();292    this.currentX = Math.floor(GRID_WIDTH / 2) - Math.floor(this.currentPiece.shape[0].length / 2);293    this.currentY = 0;294    if (this.checkCollision(this.currentPiece, this.currentX, this.currentY)) { this.gameOver = true; this.handleGameOver(); }295    this.updateNextPieceDisplay();296  }297 298  update(time, delta) {299    if (this.gameOver || !this.inputEnabled) return;300 301    // Pause check - always available302    if (Phaser.Input.Keyboard.JustDown(this.pKey)) {303      this.togglePause();304    }305 306    if (this.clearing || this.paused) return;307 308    this.handleInput();309    this.dropCounter++;310    if (this.dropCounter >= this.dropInterval) { this.dropCounter = 0; this.moveDown(); }311    this.renderPiece();312  }313 314  togglePause() {315    this.paused = !this.paused;316    if (this.paused) {317      this.pauseOverlay = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.8);318      this.pauseOverlay.setDepth(100);319      this.pauseText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, 'PAUSED');320      this.pauseText.setOrigin(0.5).setDepth(101);321      this.pauseHintText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 12, 'PRESS P');322      this.pauseHintText.setOrigin(0.5).setDepth(101);323      if (this.currentMusic) this.currentMusic.pause();324    } else {325      if (this.pauseOverlay) { this.pauseOverlay.destroy(); this.pauseOverlay = null; }326      if (this.pauseText) { this.pauseText.destroy(); this.pauseText = null; }327      if (this.pauseHintText) { this.pauseHintText.destroy(); this.pauseHintText = null; }328      if (this.currentMusic) this.currentMusic.resume();329    }330  }331 332  handleInput() {333    // Rotation - JustDown ensures one action per press334    if (Phaser.Input.Keyboard.JustDown(this.cursors.up)) {335      this.rotatePiece();336    }337 338    // Hard drop - JustDown ensures one action per press339    if (Phaser.Input.Keyboard.JustDown(this.spaceKey)) {340      this.hardDrop();341    }342 343    // Soft drop (down key held) - continuous action344    if (this.cursors.down.isDown) {345      if (!this.softDropping) { this.softDropping = true; this.softDropCounter = 0; }346      this.softDropCounter++;347      if (this.softDropCounter >= 2) {348        this.softDropCounter = 0;349        if (this.moveDown()) {350          this.score += SCORES.SOFT_DROP;351          this.updateUI();352          SoundGenerator.playSoftDrop();353        }354      }355    } else {356      this.softDropping = false;357      this.softDropCounter = 0;358    }359 360    // Decrement grace counters361    if (this.leftGraceCounter > 0) this.leftGraceCounter--;362    if (this.rightGraceCounter > 0) this.rightGraceCounter--;363 364    // LEFT - JustDown for first press, then auto-repeat when held365    if (Phaser.Input.Keyboard.JustDown(this.cursors.left) && this.leftGraceCounter === 0) {366      this.moveLeft();367      this.leftHoldCounter = 0;368      this.leftGraceCounter = this.moveGracePeriod;369    } else if (this.cursors.left.isDown && this.leftGraceCounter === 0) {370      this.leftHoldCounter++;371      if (this.leftHoldCounter >= this.dasDelay && (this.leftHoldCounter - this.dasDelay) % this.dasSpeed === 0) {372        this.moveLeft();373        this.leftGraceCounter = this.moveGracePeriod;374      }375    } else if (!this.cursors.left.isDown) {376      this.leftHoldCounter = 0;377    }378 379    // RIGHT - JustDown for first press, then auto-repeat when held380    if (Phaser.Input.Keyboard.JustDown(this.cursors.right) && this.rightGraceCounter === 0) {381      this.moveRight();382      this.rightHoldCounter = 0;383      this.rightGraceCounter = this.moveGracePeriod;384    } else if (this.cursors.right.isDown && this.rightGraceCounter === 0) {385      this.rightHoldCounter++;386      if (this.rightHoldCounter >= this.dasDelay && (this.rightHoldCounter - this.dasDelay) % this.dasSpeed === 0) {387        this.moveRight();388        this.rightGraceCounter = this.moveGracePeriod;389      }390    } else if (!this.cursors.right.isDown) {391      this.rightHoldCounter = 0;392    }393  }394 395  moveLeft() { if (!this.checkCollision(this.currentPiece, this.currentX - 1, this.currentY)) { this.currentX--; SoundGenerator.playMove(); } }396  moveRight() { if (!this.checkCollision(this.currentPiece, this.currentX + 1, this.currentY)) { this.currentX++; SoundGenerator.playMove(); } }397  moveDown() { if (!this.checkCollision(this.currentPiece, this.currentX, this.currentY + 1)) { this.currentY++; return true; } else { this.lockPiece(); return false; } }398  hardDrop() { while (!this.checkCollision(this.currentPiece, this.currentX, this.currentY + 1)) this.currentY++; SoundGenerator.playDrop(); this.lockPiece(); }399 400  rotatePiece() {401    const rotated = this.getRotatedPiece(this.currentPiece);402 403    // Try rotation at current position404    if (!this.checkCollision(rotated, this.currentX, this.currentY)) {405      this.currentPiece = rotated;406      SoundGenerator.playRotate();407      return;408    }409 410    // Wall kick: try shifting right411    if (!this.checkCollision(rotated, this.currentX + 1, this.currentY)) {412      this.currentPiece = rotated;413      this.currentX++;414      SoundGenerator.playRotate();415      return;416    }417 418    // Wall kick: try shifting left419    if (!this.checkCollision(rotated, this.currentX - 1, this.currentY)) {420      this.currentPiece = rotated;421      this.currentX--;422      SoundGenerator.playRotate();423      return;424    }425 426    // Wall kick: try shifting right 2 spaces (for I-piece)427    if (!this.checkCollision(rotated, this.currentX + 2, this.currentY)) {428      this.currentPiece = rotated;429      this.currentX += 2;430      SoundGenerator.playRotate();431      return;432    }433 434    // Wall kick: try shifting left 2 spaces (for I-piece)435    if (!this.checkCollision(rotated, this.currentX - 2, this.currentY)) {436      this.currentPiece = rotated;437      this.currentX -= 2;438      SoundGenerator.playRotate();439      return;440    }441 442    // Rotation failed - no valid position found443  }444 445  getRotatedPiece(piece) {446    const rotated = JSON.parse(JSON.stringify(piece));447    const shape = piece.shape;448    const rows = shape.length;449    const cols = shape[0].length;450    const newShape = [];451    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]; }452    rotated.shape = newShape;453    return rotated;454  }455 456  checkCollision(piece, x, y) {457    const shape = piece.shape;458    for (let row = 0; row < shape.length; row++) {459      for (let col = 0; col < shape[row].length; col++) {460        if (shape[row][col]) {461          const gridX = x + col;462          const gridY = y + row;463          if (gridX < 0 || gridX >= GRID_WIDTH || gridY >= GRID_HEIGHT) return true;464          if (gridY >= 0 && this.grid[gridY][gridX]) return true;465        }466      }467    }468    return false;469  }470 471  lockPiece() {472    const shape = this.currentPiece.shape;473    for (let row = 0; row < shape.length; row++) {474      for (let col = 0; col < shape[row].length; col++) {475        if (shape[row][col]) {476          const gridX = this.currentX + col;477          const gridY = this.currentY + row;478          if (gridY >= 0) this.grid[gridY][gridX] = this.currentPiece.name;479        }480      }481    }482    this.checkAndClearLines();483  }484 485  checkAndClearLines() {486    // Find complete lines - a line is complete ONLY if every cell is filled487    const completeLines = [];488    for (let y = 0; y < GRID_HEIGHT; y++) {489      let isComplete = true;490      for (let x = 0; x < GRID_WIDTH; x++) {491        if (!this.grid[y][x]) {492          isComplete = false;493          break;494        }495      }496      if (isComplete) {497        console.log(`Line ${y} is complete:`, JSON.stringify(this.grid[y]));498        completeLines.push(y);499      }500    }501 502    if (completeLines.length > 0) {503      console.log('Complete lines found:', completeLines);504      console.log('Grid state:', JSON.stringify(this.grid));505    }506 507    if (completeLines.length === 0) {508      this.spawnPiece();509      this.redrawGrid();510      return;511    }512 513    // Block game updates during line clear514    this.clearing = true;515 516    // Play sound based on number of lines cleared517    SoundGenerator.playLineClear(completeLines.length);518 519    // Show the locked piece first520    this.redrawGrid();521 522    // Run the line clear animation, then apply changes523    this.animateLineClear(completeLines);524  }525 526  animateLineClear(completeLines) {527    // Create crush animation for each block528    const crushSprites = [];529    const texturesToCleanup = [];530 531    completeLines.forEach(y => {532      for (let x = 0; x < GRID_WIDTH; x++) {533        const blockType = this.grid[y][x];534        if (!blockType) continue;535 536        const px = PLAY_AREA_X + x * BLOCK_SIZE;537        const py = PLAY_AREA_Y + y * BLOCK_SIZE;538 539        // Get the block's color from the palette540        const colorIndex = blockType - 1;541        const color = this.colorPalette[colorIndex % this.colorPalette.length];542 543        // Create unique crush animation frames for this specific block instance544        const uniqueId = `${Date.now()}-${x}-${y}-${Math.random().toString(36).substr(2, 9)}`;545        const frames = [];546        for (let f = 0; f < 5; f++) {547          const frameKey = `crush-${uniqueId}-${f}`;548          SpriteBlockRenderer.createCrushTexture(this, color, f, frameKey);549          frames.push(frameKey);550          texturesToCleanup.push(frameKey);551        }552 553        // Create sprite starting with frame 4 (most intact)554        const sprite = this.add.sprite(px, py, frames[4]).setOrigin(0, 0);555        sprite.setDepth(50);556        crushSprites.push({ sprite, frames });557      }558    });559 560    // Cycle through the 5 crush frames in REVERSE: 4 -> 3 -> 2 -> 1 -> 0561    let frameCounter = 4;562 563    this.time.addEvent({564      delay: 75, // 75ms per frame (twice as fast)565      repeat: 4, // repeat 4 times = 5 total callbacks (frames 4,3,2,1,0)566      callback: () => {567        if (frameCounter > 0) {568          frameCounter--;569          crushSprites.forEach(crushData => {570            crushData.sprite.setTexture(crushData.frames[frameCounter]);571          });572        }573      }574    });575 576    // After all 5 frames (4 shows immediately, then 3,2,1,0 at 75ms each = 300ms total), clean up577    this.time.delayedCall(350, () => {578      crushSprites.forEach(crushData => {579        crushData.sprite.destroy();580      });581 582      // Clean up all textures583      texturesToCleanup.forEach(frameKey => {584        if (this.textures.exists(frameKey)) {585          this.textures.remove(frameKey);586        }587      });588 589      this.finishLineClear(completeLines);590    });591  }592 593  finishLineClear(completeLines) {594    // Apply the grid changes first595    const validLines = completeLines.filter(y => {596      if (y < 0 || y >= GRID_HEIGHT) return false;597      for (let x = 0; x < GRID_WIDTH; x++) {598        if (!this.grid[y][x]) return false;599      }600      return true;601    });602 603    if (validLines.length === 0) {604      console.warn('No valid lines to clear after validation');605      this.clearing = false;606      this.spawnPiece();607      this.redrawGrid();608      return;609    }610 611    // Build new grid612    const newGrid = [];613    const linesToRemove = new Set(validLines);614 615    for (let i = 0; i < validLines.length; i++) {616      newGrid.push(new Array(GRID_WIDTH).fill(0));617    }618 619    for (let y = 0; y < GRID_HEIGHT; y++) {620      if (!linesToRemove.has(y)) {621        newGrid.push([...this.grid[y]]);622      }623    }624 625    this.grid = newGrid;626 627    // Now animate the falling blocks628    // Rebuild sprites from new grid state629    this.redrawGrid();630 631    // Animate all sprites falling into place632    const sortedLines = [...validLines].sort((a, b) => a - b);633 634    this.blockSprites.forEach(sprite => {635      const spriteGridY = Math.floor((sprite.y - PLAY_AREA_Y) / BLOCK_SIZE);636 637      // Count how many cleared lines were below this sprite's ORIGINAL position638      let linesBelowCount = 0;639      sortedLines.forEach(clearedY => {640        if (clearedY > spriteGridY - validLines.length) {641          linesBelowCount++;642        }643      });644 645      if (linesBelowCount > 0) {646        // Start sprite higher, then animate down to current position647        const startY = sprite.y - (linesBelowCount * BLOCK_SIZE);648        sprite.y = startY;649 650        this.tweens.add({651          targets: sprite,652          y: sprite.y + (linesBelowCount * BLOCK_SIZE),653          duration: 150,654          ease: 'Bounce.easeOut'655        });656      }657    });658 659    // Wait for fall animation, then finish660    this.time.delayedCall(160, () => {661      this.finishScoring(validLines);662    });663  }664 665  finishScoring(validLines) {666    // Update score667    this.lines += validLines.length;668    const levelMultiplier = this.level;669    switch (validLines.length) {670      case 1: this.score += SCORES.SINGLE * levelMultiplier; break;671      case 2: this.score += SCORES.DOUBLE * levelMultiplier; break;672      case 3: this.score += SCORES.TRIPLE * levelMultiplier; break;673      case 4: this.score += SCORES.TETRIS * levelMultiplier; break;674    }675 676    // Check for perfect clear (entire grid is empty)677    const isPerfectClear = this.grid.every(row => row.every(cell => cell === 0));678    if (isPerfectClear) {679      this.score += SCORES.PERFECT_CLEAR * levelMultiplier;680      // Show perfect clear message681      const perfectText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, 'PERFECT CLEAR!', 12);682      perfectText.setOrigin(0.5);683      perfectText.setDepth(150);684      perfectText.setTint(0xFFD700); // Gold color685 686      // Animate the text687      this.tweens.add({688        targets: perfectText,689        scale: 1.5,690        alpha: 0,691        duration: 2000,692        ease: 'Power2',693        onComplete: () => perfectText.destroy()694      });695 696      // Play special sound697      SoundGenerator.playLevelUp();698    }699 700    // Check for level up701    const newLevel = Math.min(MAX_LEVEL, Math.floor(this.lines / CONFIG.LINES_PER_LEVEL) + 1);702    if (newLevel > this.level) {703      this.level = newLevel;704      this.dropInterval = LEVEL_SPEEDS[this.level - 1];705      SoundGenerator.playLevelUp();706 707      // Exciting level transition!708      this.showLevelTransition(newLevel);709    } else {710      this.updateUI();711      this.clearing = false;712      this.spawnPiece();713    }714  }715 716 717 718  showLevelTransition(newLevel) {719    // Keep game paused during transition720    this.clearing = true;721 722    // Black screen overlay723    const blackScreen = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000);724    blackScreen.setDepth(200);725    blackScreen.setAlpha(0);726 727    // Fade to black728    this.tweens.add({729      targets: blackScreen,730      alpha: 1,731      duration: 300,732      ease: 'Power2',733      onComplete: () => {734        // Pre-load the new level's palette and create preview blocks735        const backdropKey = `backdrop-${newLevel}`;736        const rawPalette = ColorExtractor.extractPalette(this, backdropKey);737        const newPalette = SpriteBlockRenderer.enhancePalette(rawPalette);738 739        // Level up text740        const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 60, `LEVEL ${newLevel}`, 20);741        levelText.setOrigin(0.5);742        levelText.setDepth(201);743        levelText.setAlpha(0);744 745        // Subtitle - show level title746        const levelTitle = LEVEL_TITLES[newLevel] || 'Unknown';747        const subtitle = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 85, levelTitle, 10);748        subtitle.setOrigin(0.5);749        subtitle.setDepth(201);750        subtitle.setAlpha(0);751 752        // Create preview blocks showing new level's style753        const previewBlocks = [];754        const startX = GAME_WIDTH / 2 + BORDER_OFFSET - 32; // Center 8 blocks (8*8 = 64px wide)755        const startY = 120;756 757        for (let i = 0; i < 7; i++) {758          const x = startX + i * 10;759          const y = startY;760          const blockKey = `preview-block-${i}`;761 762          // Create block texture with new level's palette763          SpriteBlockRenderer.createBlockTexture(this, newPalette, newLevel, blockKey, i);764 765          const block = this.add.sprite(x, y, blockKey).setOrigin(0, 0);766          block.setDepth(201);767          block.setAlpha(0);768          block.setScale(0.5);769          previewBlocks.push({ sprite: block, key: blockKey });770        }771 772        // Animate text and blocks in773        this.tweens.add({774          targets: [levelText, subtitle],775          alpha: 1,776          duration: 400,777          ease: 'Power2'778        });779 780        this.tweens.add({781          targets: previewBlocks.map(b => b.sprite),782          alpha: 1,783          scale: 1,784          duration: 500,785          delay: 200,786          ease: 'Back.easeOut',787          onComplete: () => {788            // Hold for a moment789            this.time.delayedCall(1200, () => {790              // Fade out text and preview blocks only (keep black screen)791              this.tweens.add({792                targets: [levelText, subtitle, ...previewBlocks.map(b => b.sprite)],793                alpha: 0,794                duration: 300,795                onComplete: () => {796                  // Clean up text and preview blocks797                  levelText.destroy();798                  subtitle.destroy();799                  previewBlocks.forEach(b => {800                    b.sprite.destroy();801                    if (this.textures.exists(b.key)) {802                      this.textures.remove(b.key);803                    }804                  });805 806                  // Black screen stays for a moment807                  this.time.delayedCall(300, () => {808                    // Destroy old level elements while screen is black809                    this.blockSprites.forEach(sprite => sprite.destroy());810                    this.blockSprites = [];811                    this.ghostSprites.forEach(sprite => sprite.destroy());812                    this.ghostSprites = [];813 814                    // Load new level (no intro yet)815                    this.loadLevel(newLevel, false);816                    this.updateUI();817                    this.clearing = false;818                    this.spawnPiece();819 820                    // IMMEDIATELY hide UI containers before fading out black screen821                    if (this.playAreaContainer) {822                      this.playAreaContainer.y = -GAME_HEIGHT;823                    }824                    if (this.uiPanelContainer) {825                      this.uiPanelContainer.y = -GAME_HEIGHT;826                    }827                    this.blockSprites.forEach(sprite => sprite.setVisible(false));828                    this.ghostSprites.forEach(sprite => sprite.setVisible(false));829                    this.inputEnabled = false;830 831                    // Fade out black screen to reveal ONLY the backdrop832                    this.tweens.add({833                      targets: blackScreen,834                      alpha: 0,835                      duration: 500,836                      ease: 'Power2',837                      onComplete: () => {838                        blackScreen.destroy();839                        // Now show the intro animation (UI falls in)840                        // Wait 1 second showing just the backdrop841                        this.time.delayedCall(1000, () => {842                          // Play woosh sound843                          SoundGenerator.playWoosh();844 845                          // Animate play area falling in846                          if (this.playAreaContainer) {847                            this.tweens.add({848                              targets: this.playAreaContainer,849                              y: 0,850                              duration: 600,851                              ease: 'Bounce.easeOut'852                            });853                          }854 855                          // Animate UI panel falling in (slightly delayed)856                          if (this.uiPanelContainer) {857                            this.tweens.add({858                              targets: this.uiPanelContainer,859                              y: 0,860                              duration: 600,861                              delay: 100,862                              ease: 'Bounce.easeOut',863                              onComplete: () => {864                                // Show blocks and re-enable input after animations complete865                                this.blockSprites.forEach(sprite => sprite.setVisible(true));866                                this.ghostSprites.forEach(sprite => sprite.setVisible(true));867                                this.inputEnabled = true;868                              }869                            });870                          }871                        });872                      }873                    });874                  });875                }876              });877            });878          }879        });880      }881    });882  }883 884  redrawGrid() {885    this.blockSprites.forEach(sprite => sprite.destroy());886    this.blockSprites = [];887    for (let y = 0; y < GRID_HEIGHT; y++) {888      for (let x = 0; x < GRID_WIDTH; x++) {889        if (this.grid[y][x]) {890          const blockType = this.grid[y][x];891          const sprite = this.add.sprite(PLAY_AREA_X + x * BLOCK_SIZE, PLAY_AREA_Y + y * BLOCK_SIZE, `block-${blockType}`).setOrigin(0, 0);892          sprite.setDepth(2);893          this.blockSprites.push(sprite);894        }895      }896    }897  }898 899  renderPiece() {900    this.blockSprites.forEach(sprite => { if (sprite.getData('current')) sprite.destroy(); });901    this.blockSprites = this.blockSprites.filter(s => !s.getData('current'));902    this.ghostSprites.forEach(sprite => sprite.destroy());903    this.ghostSprites = [];904    if (!this.currentPiece) return;905    if (this.level === 1) {906      let ghostY = this.currentY;907      while (!this.checkCollision(this.currentPiece, this.currentX, ghostY + 1)) ghostY++;908      const shape = this.currentPiece.shape;909      for (let row = 0; row < shape.length; row++) {910        for (let col = 0; col < shape[row].length; col++) {911          if (shape[row][col]) {912            const x = PLAY_AREA_X + (this.currentX + col) * BLOCK_SIZE;913            const y = PLAY_AREA_Y + (ghostY + row) * BLOCK_SIZE;914            const sprite = this.add.sprite(x, y, `block-${this.currentPiece.name}`).setOrigin(0, 0);915            sprite.setAlpha(0.3);916            sprite.setDepth(1);917            this.ghostSprites.push(sprite);918          }919        }920      }921    }922    const shape = this.currentPiece.shape;923    for (let row = 0; row < shape.length; row++) {924      for (let col = 0; col < shape[row].length; col++) {925        if (shape[row][col]) {926          const x = PLAY_AREA_X + (this.currentX + col) * BLOCK_SIZE;927          const y = PLAY_AREA_Y + (this.currentY + row) * BLOCK_SIZE;928          const sprite = this.add.sprite(x, y, `block-${this.currentPiece.name}`).setOrigin(0, 0);929          sprite.setData('current', true);930          sprite.setDepth(2);931          this.blockSprites.push(sprite);932        }933      }934    }935  }936 937  updateNextPieceDisplay() {938    if (this.nextPieceSprites) this.nextPieceSprites.forEach(sprite => sprite.destroy());939    this.nextPieceSprites = [];940    if (!this.nextPiece) return;941    const shape = this.nextPiece.shape;942    const startX = this.nextPieceX;943    const startY = this.nextPieceY;944    for (let row = 0; row < shape.length; row++) {945      for (let col = 0; col < shape[row].length; col++) {946        if (shape[row][col]) {947          const x = startX + col * BLOCK_SIZE;948          const y = startY + row * BLOCK_SIZE;949          const sprite = this.add.sprite(x, y, `block-${this.nextPiece.name}`).setOrigin(0, 0);950          sprite.setDepth(20);951          this.nextPieceSprites.push(sprite);952          // Add to UI container so it animates with the panel953          if (this.uiPanelContainer) {954            this.uiPanelContainer.add(sprite);955          }956        }957      }958    }959  }960 961  updateUI() {962    const scoreStr = this.score.toString().padStart(6, '0');963    this.scoreText.setText(scoreStr);964    this.levelText.setText(this.level.toString());965    this.linesText.setText(this.lines.toString());966  }967 968  handleGameOver() {969    if (this.currentMusic) this.currentMusic.stop();970    SoundGenerator.playGameOver();971 972    // Display game over image (256x224, fills the game area)973    const gameOverImage = this.add.image(BORDER_OFFSET, 0, 'game-over');974    gameOverImage.setOrigin(0, 0);975    gameOverImage.setDisplaySize(GAME_WIDTH, GAME_HEIGHT);976    gameOverImage.setDepth(100);977    gameOverImage.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);978 979    this.input.keyboard.once('keydown-SPACE', () => {980      this.scene.start('PreloadScene');981    });982  }983}984