mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-17 23:55:21 +01:00
Add enhanced game over animation with falling blocks, screen shake, and dynamic text effects
This commit is contained in:
parent
7dccad8d12
commit
94e8d313c2
2 changed files with 268 additions and 37 deletions
|
@ -29,6 +29,8 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@ -320,6 +322,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
* Show game over screen
|
* Show game over screen
|
||||||
*/
|
*/
|
||||||
private fun showGameOver(score: Int) {
|
private fun showGameOver(score: Int) {
|
||||||
|
Log.d("MainActivity", "Showing game over screen with score: $score")
|
||||||
val gameTime = System.currentTimeMillis() - gameStartTime
|
val gameTime = System.currentTimeMillis() - gameStartTime
|
||||||
|
|
||||||
// Update session stats
|
// Update session stats
|
||||||
|
@ -370,30 +373,36 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (isSoundEnabled) {
|
if (isSoundEnabled) {
|
||||||
gameMusic.playGameOver()
|
gameMusic.playGameOver()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First trigger the animation in the game view
|
||||||
|
Log.d("MainActivity", "Triggering game over animation")
|
||||||
gameView.startGameOverAnimation()
|
gameView.startGameOverAnimation()
|
||||||
|
|
||||||
// Show progression screen first with XP animation
|
// Wait a moment before showing progression screen to let animation be visible
|
||||||
showProgressionScreen(xpGained, newRewards)
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
// Show progression screen first with XP animation
|
||||||
// Override the continue button behavior if high score needs to be shown
|
showProgressionScreen(xpGained, newRewards)
|
||||||
val originalOnContinue = progressionScreen.onContinue
|
|
||||||
|
// Override the continue button behavior if high score needs to be shown
|
||||||
progressionScreen.onContinue = {
|
val originalOnContinue = progressionScreen.onContinue
|
||||||
// If this is a high score, show high score entry screen
|
|
||||||
if (highScoreManager.isHighScore(score)) {
|
progressionScreen.onContinue = {
|
||||||
showingHighScore = true
|
// If this is a high score, show high score entry screen
|
||||||
showHighScoreEntry(score)
|
if (highScoreManager.isHighScore(score)) {
|
||||||
} else {
|
showingHighScore = true
|
||||||
// Just show game over screen normally
|
showHighScoreEntry(score)
|
||||||
progressionScreen.visibility = View.GONE
|
} else {
|
||||||
binding.gameOverContainer.visibility = View.VISIBLE
|
// Just show game over screen normally
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
// Update theme selector if new themes were unlocked
|
binding.gameOverContainer.visibility = View.VISIBLE
|
||||||
if (newRewards.any { it.contains("Theme") }) {
|
|
||||||
updateThemeSelector()
|
// 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 to indicate game over
|
||||||
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
|
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
|
||||||
|
|
|
@ -181,6 +181,15 @@ class GameView @JvmOverloads constructor(
|
||||||
private var gameOverAnimator: ValueAnimator? = null
|
private var gameOverAnimator: ValueAnimator? = null
|
||||||
private var gameOverAlpha = 0f
|
private var gameOverAlpha = 0f
|
||||||
private var isGameOverAnimating = false
|
private var isGameOverAnimating = false
|
||||||
|
// Add new game over animation properties
|
||||||
|
private var gameOverBlocksY = mutableListOf<Float>()
|
||||||
|
private var gameOverBlocksX = mutableListOf<Float>()
|
||||||
|
private var gameOverBlocksRotation = mutableListOf<Float>()
|
||||||
|
private var gameOverBlocksSpeed = mutableListOf<Float>()
|
||||||
|
private var gameOverBlocksSize = mutableListOf<Float>()
|
||||||
|
private var gameOverColorTransition = 0f
|
||||||
|
private var gameOverShakeAmount = 0f
|
||||||
|
private var gameOverFinalAlpha = 0.8f
|
||||||
|
|
||||||
private val ghostPaint = Paint().apply {
|
private val ghostPaint = Paint().apply {
|
||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
|
@ -321,6 +330,17 @@ class GameView @JvmOverloads constructor(
|
||||||
* Start the game
|
* Start the game
|
||||||
*/
|
*/
|
||||||
fun start() {
|
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
|
isPaused = false
|
||||||
isRunning = true
|
isRunning = true
|
||||||
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
||||||
|
@ -343,6 +363,18 @@ class GameView @JvmOverloads constructor(
|
||||||
fun reset() {
|
fun reset() {
|
||||||
isRunning = false
|
isRunning = false
|
||||||
isPaused = true
|
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.reset()
|
||||||
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
||||||
handler.removeCallbacks(gameLoopRunnable)
|
handler.removeCallbacks(gameLoopRunnable)
|
||||||
|
@ -354,9 +386,17 @@ class GameView @JvmOverloads constructor(
|
||||||
*/
|
*/
|
||||||
private fun update() {
|
private fun update() {
|
||||||
if (gameBoard.isGameOver) {
|
if (gameBoard.isGameOver) {
|
||||||
isRunning = false
|
// Only trigger game over handling once when transitioning to game over state
|
||||||
isPaused = true
|
if (isRunning) {
|
||||||
onGameOver?.invoke(gameBoard.score)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,30 +479,128 @@ class GameView @JvmOverloads constructor(
|
||||||
|
|
||||||
// Draw game over effect if animating
|
// Draw game over effect if animating
|
||||||
if (isGameOverAnimating) {
|
if (isGameOverAnimating) {
|
||||||
|
// First layer - full screen glow
|
||||||
val gameOverPaint = Paint().apply {
|
val gameOverPaint = Paint().apply {
|
||||||
color = Color.WHITE
|
color = Color.RED // Change to red for more striking game over indication
|
||||||
alpha = (200 * gameOverAlpha).toInt() // Increased from 128 to 200
|
alpha = (230 * gameOverAlpha).toInt() // Increased opacity
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
style = Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
// Only apply blur if alpha is greater than 0
|
// Only apply blur if alpha is greater than 0
|
||||||
if (gameOverAlpha > 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)
|
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 {
|
val gameOverPaint2 = Paint().apply {
|
||||||
color = Color.WHITE
|
// Transition from bright red to theme color
|
||||||
alpha = (100 * gameOverAlpha).toInt()
|
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
|
isAntiAlias = true
|
||||||
style = Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
// Only apply blur if alpha is greater than 0
|
// Only apply blur if alpha is greater than 0
|
||||||
if (gameOverAlpha > 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)
|
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() {
|
fun startGameOverAnimation() {
|
||||||
Log.d(TAG, "Starting game over animation")
|
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
|
// Cancel any existing animations
|
||||||
pulseAnimator?.cancel()
|
pulseAnimator?.cancel()
|
||||||
gameOverAnimator?.cancel()
|
gameOverAnimator?.cancel()
|
||||||
|
@ -1172,27 +1316,105 @@ class GameView @JvmOverloads constructor(
|
||||||
// Trigger haptic feedback
|
// Trigger haptic feedback
|
||||||
gameHaptics?.vibrateForGameOver()
|
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
|
// Create new game over animation
|
||||||
gameOverAnimator = ValueAnimator.ofFloat(0f, 1f, 0.8f).apply {
|
gameOverAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||||
duration = 1000L // 1 second total
|
duration = 3000L // Increased from 2000L (2 seconds) to 3000L (3 seconds)
|
||||||
interpolator = LinearInterpolator()
|
interpolator = LinearInterpolator()
|
||||||
|
|
||||||
addUpdateListener { animation ->
|
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
|
isGameOverAnimating = true
|
||||||
invalidate()
|
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() {
|
addListener(object : android.animation.AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||||
isGameOverAnimating = false
|
Log.d(TAG, "Game over animation ended - Final alpha: $gameOverFinalAlpha")
|
||||||
gameOverAlpha = 0.8f // Keep at 80% opacity
|
isGameOverAnimating = true // Keep true to maintain visibility
|
||||||
|
gameOverAlpha = gameOverFinalAlpha // Keep at 80% opacity
|
||||||
invalidate()
|
invalidate()
|
||||||
Log.d(TAG, "Game over animation ended")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
gameOverAnimator?.start()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue