diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index abfc24c..754ec2d 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -29,6 +29,8 @@ import androidx.activity.result.contract.ActivityResultContracts import android.graphics.Rect import android.util.Log import android.view.KeyEvent +import android.os.Handler +import android.os.Looper class MainActivity : AppCompatActivity() { @@ -320,6 +322,7 @@ class MainActivity : AppCompatActivity() { * Show game over screen */ private fun showGameOver(score: Int) { + Log.d("MainActivity", "Showing game over screen with score: $score") val gameTime = System.currentTimeMillis() - gameStartTime // Update session stats @@ -370,30 +373,36 @@ class MainActivity : AppCompatActivity() { if (isSoundEnabled) { gameMusic.playGameOver() } + + // First trigger the animation in the game view + Log.d("MainActivity", "Triggering game over animation") gameView.startGameOverAnimation() - // Show progression screen first with XP animation - showProgressionScreen(xpGained, newRewards) - - // Override the continue button behavior if high score needs to be shown - val originalOnContinue = progressionScreen.onContinue - - progressionScreen.onContinue = { - // If this is a high score, show high score entry screen - if (highScoreManager.isHighScore(score)) { - showingHighScore = true - showHighScoreEntry(score) - } else { - // Just show game over screen normally - progressionScreen.visibility = View.GONE - binding.gameOverContainer.visibility = View.VISIBLE - - // Update theme selector if new themes were unlocked - if (newRewards.any { it.contains("Theme") }) { - updateThemeSelector() + // Wait a moment before showing progression screen to let animation be visible + Handler(Looper.getMainLooper()).postDelayed({ + // Show progression screen first with XP animation + showProgressionScreen(xpGained, newRewards) + + // Override the continue button behavior if high score needs to be shown + val originalOnContinue = progressionScreen.onContinue + + progressionScreen.onContinue = { + // If this is a high score, show high score entry screen + if (highScoreManager.isHighScore(score)) { + showingHighScore = true + showHighScoreEntry(score) + } else { + // Just show game over screen normally + progressionScreen.visibility = View.GONE + binding.gameOverContainer.visibility = View.VISIBLE + + // Update theme selector if new themes were unlocked + if (newRewards.any { it.contains("Theme") }) { + updateThemeSelector() + } } } - } + }, 2000) // Increased from 1000ms (1 second) to 2000ms (2 seconds) // Vibrate to indicate game over vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 741b662..24e8f0c 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -181,6 +181,15 @@ class GameView @JvmOverloads constructor( private var gameOverAnimator: ValueAnimator? = null private var gameOverAlpha = 0f private var isGameOverAnimating = false + // Add new game over animation properties + private var gameOverBlocksY = mutableListOf() + private var gameOverBlocksX = mutableListOf() + private var gameOverBlocksRotation = mutableListOf() + private var gameOverBlocksSpeed = mutableListOf() + private var gameOverBlocksSize = mutableListOf() + private var gameOverColorTransition = 0f + private var gameOverShakeAmount = 0f + private var gameOverFinalAlpha = 0.8f private val ghostPaint = Paint().apply { style = Paint.Style.STROKE @@ -321,6 +330,17 @@ class GameView @JvmOverloads constructor( * Start the game */ fun start() { + // Reset game over animation state + isGameOverAnimating = false + gameOverAlpha = 0f + gameOverBlocksY.clear() + gameOverBlocksX.clear() + gameOverBlocksRotation.clear() + gameOverBlocksSpeed.clear() + gameOverBlocksSize.clear() + gameOverColorTransition = 0f + gameOverShakeAmount = 0f + isPaused = false isRunning = true gameBoard.startGame() // Add this line to ensure a new piece is spawned @@ -343,6 +363,18 @@ class GameView @JvmOverloads constructor( fun reset() { isRunning = false isPaused = true + + // Reset game over animation state + isGameOverAnimating = false + gameOverAlpha = 0f + gameOverBlocksY.clear() + gameOverBlocksX.clear() + gameOverBlocksRotation.clear() + gameOverBlocksSpeed.clear() + gameOverBlocksSize.clear() + gameOverColorTransition = 0f + gameOverShakeAmount = 0f + gameBoard.reset() gameBoard.startGame() // Add this line to ensure a new piece is spawned handler.removeCallbacks(gameLoopRunnable) @@ -354,9 +386,17 @@ class GameView @JvmOverloads constructor( */ private fun update() { if (gameBoard.isGameOver) { - isRunning = false - isPaused = true - onGameOver?.invoke(gameBoard.score) + // Only trigger game over handling once when transitioning to game over state + if (isRunning) { + Log.d(TAG, "Game has ended - transitioning to game over state") + isRunning = false + isPaused = true + + // Always trigger animation for each game over + Log.d(TAG, "Triggering game over animation from update()") + startGameOverAnimation() + onGameOver?.invoke(gameBoard.score) + } return } @@ -439,30 +479,128 @@ class GameView @JvmOverloads constructor( // Draw game over effect if animating if (isGameOverAnimating) { + // First layer - full screen glow val gameOverPaint = Paint().apply { - color = Color.WHITE - alpha = (200 * gameOverAlpha).toInt() // Increased from 128 to 200 + color = Color.RED // Change to red for more striking game over indication + alpha = (230 * gameOverAlpha).toInt() // Increased opacity isAntiAlias = true style = Paint.Style.FILL // Only apply blur if alpha is greater than 0 if (gameOverAlpha > 0) { - maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased from 48f to 64f + maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) } } + + // Apply screen shake if active + if (gameOverShakeAmount > 0) { + canvas.save() + val shakeOffsetX = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 // Doubled for more visible shake + val shakeOffsetY = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 + canvas.translate(shakeOffsetX.toFloat(), shakeOffsetY.toFloat()) + } + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint) - // Add a second layer for more intensity + // Second layer - color transition effect val gameOverPaint2 = Paint().apply { - color = Color.WHITE - alpha = (100 * gameOverAlpha).toInt() + // Transition from bright red to theme color + val transitionColor = if (gameOverColorTransition < 0.5f) { + Color.RED + } else { + val transition = (gameOverColorTransition - 0.5f) * 2f + val red = Color.red(currentThemeColor) + val green = Color.green(currentThemeColor) + val blue = Color.blue(currentThemeColor) + Color.argb( + (150 * gameOverAlpha).toInt(), // Increased opacity + (255 - (255-red) * transition).toInt(), + (green * transition).toInt(), // Transition from 0 (red) to theme green + (blue * transition).toInt() // Transition from 0 (red) to theme blue + ) + } + color = transitionColor + alpha = (150 * gameOverAlpha).toInt() // Increased opacity isAntiAlias = true style = Paint.Style.FILL // Only apply blur if alpha is greater than 0 if (gameOverAlpha > 0) { - maskFilter = BlurMaskFilter(32f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) + maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased blur } } canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint2) + + // Draw "GAME OVER" text + if (gameOverAlpha > 0.5f) { + val textPaint = Paint().apply { + color = Color.WHITE + alpha = (255 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt() + isAntiAlias = true + textSize = blockSize * 1.5f // Reduced from 2f to 1.5f to fit on screen + textAlign = Paint.Align.CENTER + typeface = android.graphics.Typeface.DEFAULT_BOLD + style = Paint.Style.FILL_AND_STROKE + strokeWidth = blockSize * 0.08f // Proportionally reduced from 0.1f + } + + // Draw text with glow + val glowPaint = Paint(textPaint).apply { + maskFilter = BlurMaskFilter(blockSize * 0.4f, BlurMaskFilter.Blur.NORMAL) // Reduced from 0.5f + alpha = (200 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt() + color = Color.RED + strokeWidth = blockSize * 0.15f // Reduced from 0.2f + } + + val xPos = width / 2f + val yPos = height / 3f + + // Measure text width to check if it fits + val textWidth = textPaint.measureText("GAME OVER") + + // If text would still be too wide, scale it down further + if (textWidth > width * 0.9f) { + val scaleFactor = width * 0.9f / textWidth + textPaint.textSize *= scaleFactor + glowPaint.textSize *= scaleFactor + } + + canvas.drawText("GAME OVER", xPos, yPos, glowPaint) + canvas.drawText("GAME OVER", xPos, yPos, textPaint) + } + + // Draw falling blocks + if (gameOverBlocksY.isNotEmpty()) { + for (i in gameOverBlocksY.indices) { + val x = gameOverBlocksX[i] + val y = gameOverBlocksY[i] + val rotation = gameOverBlocksRotation[i] + val size = gameOverBlocksSize[i] * blockSize + + // Skip blocks that have fallen off the screen + if (y > height) continue + + // Draw each falling block with rotation + canvas.save() + canvas.translate(x, y) + canvas.rotate(rotation) + + // Create a pulsing effect for the falling blocks + val blockPaint = Paint(blockPaint) + blockPaint.alpha = (255 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.7f)).toInt() + + // Draw block with glow effect + val blockGlowPaint = Paint(blockGlowPaint) + blockGlowPaint.alpha = (200 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.5f)).toInt() + canvas.drawRect(-size/2, -size/2, size/2, size/2, blockGlowPaint) + canvas.drawRect(-size/2, -size/2, size/2, size/2, blockPaint) + + canvas.restore() + } + } + + // Reset any transformations from screen shake + if (gameOverShakeAmount > 0) { + canvas.restore() + } } } @@ -1165,6 +1303,12 @@ class GameView @JvmOverloads constructor( fun startGameOverAnimation() { Log.d(TAG, "Starting game over animation") + // Check if game over already showing + if (isGameOverAnimating && gameOverAlpha > 0.5f) { + Log.d(TAG, "Game over animation already active - skipping") + return + } + // Cancel any existing animations pulseAnimator?.cancel() gameOverAnimator?.cancel() @@ -1172,27 +1316,105 @@ class GameView @JvmOverloads constructor( // Trigger haptic feedback gameHaptics?.vibrateForGameOver() + // Force immediate visual feedback + isGameOverAnimating = true + gameOverAlpha = 0.3f + invalidate() + + // Generate falling blocks based on current board state + generateGameOverBlocks() + // Create new game over animation - gameOverAnimator = ValueAnimator.ofFloat(0f, 1f, 0.8f).apply { - duration = 1000L // 1 second total + gameOverAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = 3000L // Increased from 2000L (2 seconds) to 3000L (3 seconds) interpolator = LinearInterpolator() addUpdateListener { animation -> - gameOverAlpha = animation.animatedValue as Float + val progress = animation.animatedValue as Float + + // Main alpha transition for overlay + gameOverAlpha = when { + progress < 0.2f -> progress * 5f // Quick fade in (first 20% of animation) + else -> 1f // Hold at full opacity + } + + // Color transition effect (start after 40% of animation) + gameOverColorTransition = when { + progress < 0.4f -> 0f + progress < 0.8f -> (progress - 0.4f) * 2.5f // Transition during 40%-80% + else -> 1f + } + + // Screen shake effect (strongest at beginning, fades out) + gameOverShakeAmount = when { + progress < 0.3f -> progress * 3.33f // Ramp up + progress < 0.6f -> 1f - (progress - 0.3f) * 3.33f // Ramp down + else -> 0f // No shake + } + + // Update falling blocks + updateGameOverBlocks() + isGameOverAnimating = true invalidate() - Log.d(TAG, "Game over animation update: alpha = $gameOverAlpha") + Log.d(TAG, "Game over animation update: alpha = $gameOverAlpha, progress = $progress") } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { - isGameOverAnimating = false - gameOverAlpha = 0.8f // Keep at 80% opacity + Log.d(TAG, "Game over animation ended - Final alpha: $gameOverFinalAlpha") + isGameOverAnimating = true // Keep true to maintain visibility + gameOverAlpha = gameOverFinalAlpha // Keep at 80% opacity invalidate() - Log.d(TAG, "Game over animation ended") } }) } gameOverAnimator?.start() } + + /** + * Generate falling blocks for the game over animation based on current board state + */ + private fun generateGameOverBlocks() { + // Clear existing blocks + gameOverBlocksY.clear() + gameOverBlocksX.clear() + gameOverBlocksRotation.clear() + gameOverBlocksSpeed.clear() + gameOverBlocksSize.clear() + + // Generate 30-40 blocks across the board + val numBlocks = (30 + Math.random() * 10).toInt() + + for (i in 0 until numBlocks) { + // Start positions - distribute across the board width but clustered near the top + gameOverBlocksX.add(boardLeft + (Math.random() * gameBoard.width * blockSize).toFloat()) + gameOverBlocksY.add((boardTop - blockSize * 2 + Math.random() * height * 0.3f).toFloat()) + + // Random rotation + gameOverBlocksRotation.add((Math.random() * 360).toFloat()) + + // Random fall speed (some faster, some slower) + gameOverBlocksSpeed.add((5 + Math.random() * 15).toFloat()) + + // Slightly varied block sizes + gameOverBlocksSize.add((0.8f + Math.random() * 0.4f).toFloat()) + } + } + + /** + * Update the position of falling blocks in the game over animation + */ + private fun updateGameOverBlocks() { + for (i in gameOverBlocksY.indices) { + // Update Y position based on speed + gameOverBlocksY[i] += gameOverBlocksSpeed[i] + + // Update rotation + gameOverBlocksRotation[i] += gameOverBlocksSpeed[i] * 0.5f + + // Accelerate falling + gameOverBlocksSpeed[i] *= 1.03f + } + } }