broadfield-dev/stack-overflow-game
0
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 