From e23d33e2e215623df5eba037ceeca5cb22d23436 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sun, 30 Mar 2025 15:45:19 -0400 Subject: [PATCH 1/6] feat: improve touch controls - increase tap sensitivity for rotation and make hard drop more distinct --- .../main/java/com/mintris/game/GameView.kt | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index f1608f4..fd20729 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -150,11 +150,12 @@ class GameView @JvmOverloads constructor( private var lastHoldTime = 0L // Track when the last hold occurred private val holdCooldown = 250L // Minimum time between holds private var lockedDirection: Direction? = null // Track the locked movement direction - private val minMovementThreshold = 0.5f // Reduced from 0.75f for more sensitive movement - private val directionLockThreshold = 1.5f // Reduced from 2.5f to make direction locking less aggressive + private val minMovementThreshold = 0.3f // Reduced from 0.5f for more sensitive horizontal movement + private val directionLockThreshold = 1.0f // Reduced from 1.5f to make direction locking less aggressive private val isStrictDirectionLock = true // Re-enabled strict direction locking to prevent diagonal inputs - private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture + private val minHardDropDistance = 2.5f // Increased from 1.5f to require more deliberate hard drops private val minHoldDistance = 2.0f // Minimum distance (in blocks) for hold gesture + private val maxSoftDropDistance = 1.5f // Maximum distance for soft drop before considering hard drop // Block skin private var currentBlockSkin: String = "block_skin_1" @@ -892,9 +893,10 @@ class GameView @JvmOverloads constructor( invalidate() } } - // Check for hard drop + // Check for hard drop (must be faster and longer than soft drop) else if (deltaY > blockSize * minHardDropDistance && - abs(deltaX) / abs(deltaY) < 0.5f) { + abs(deltaX) / abs(deltaY) < 0.5f && + (deltaY / moveTime) * 1000 > minSwipeVelocity) { if (currentTime - lastHardDropTime < hardDropCooldown) { Log.d(TAG, "Hard drop blocked by cooldown - time since last: ${currentTime - lastHardDropTime}ms, cooldown: ${hardDropCooldown}ms") } else { @@ -905,10 +907,17 @@ class GameView @JvmOverloads constructor( invalidate() } } + // Check for soft drop (slower and shorter than hard drop) + else if (deltaY > blockSize * minMovementThreshold && + deltaY < blockSize * maxSoftDropDistance && + (deltaY / moveTime) * 1000 < minSwipeVelocity) { + gameBoard.softDrop() + invalidate() + } // Check for rotation (quick tap with minimal movement) - else if (moveTime < minTapTime && - abs(deltaY) < maxTapMovement && - abs(deltaX) < maxTapMovement) { + else if (moveTime < minTapTime * 1.5 && // Increased from 1.0 to 1.5 for more lenient timing + abs(deltaY) < maxTapMovement * 1.5 && // Increased from 1.0 to 1.5 for more lenient movement + abs(deltaX) < maxTapMovement * 1.5) { // Increased from 1.0 to 1.5 for more lenient movement if (currentTime - lastRotationTime >= rotationCooldown) { Log.d(TAG, "Rotation detected - moveTime: $moveTime, deltaX: $deltaX, deltaY: $deltaY") gameBoard.rotate() From 03ff049bef15a1ef0bdf1d6b0ac551ac5563cf19 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sun, 30 Mar 2025 15:54:36 -0400 Subject: [PATCH 2/6] feat: improve ghost piece visibility with three-layer design for better accessibility --- .../main/java/com/mintris/game/GameView.kt | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index fd20729..27e38a0 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -179,6 +179,26 @@ class GameView @JvmOverloads constructor( private var isPulsing = false private var linesToPulse = mutableListOf() // Track which lines are being cleared + private val ghostPaint = Paint().apply { + style = Paint.Style.STROKE + strokeWidth = 2f + color = Color.WHITE + alpha = 180 // Increased from 100 for better visibility + } + + private val ghostBackgroundPaint = Paint().apply { + style = Paint.Style.FILL + color = Color.WHITE + alpha = 30 // Very light background for better contrast + } + + private val ghostBorderPaint = Paint().apply { + style = Paint.Style.STROKE + strokeWidth = 1f + color = Color.WHITE + alpha = 100 // Subtle border for better definition + } + init { // Start with paused state pause() @@ -578,15 +598,43 @@ class GameView @JvmOverloads constructor( val piece = gameBoard.getCurrentPiece() ?: return val ghostY = gameBoard.getGhostY() + // Draw semi-transparent background for each block for (y in 0 until piece.getHeight()) { for (x in 0 until piece.getWidth()) { if (piece.isBlockAt(x, y)) { val boardX = piece.x + x val boardY = ghostY + y - // Draw ghost piece regardless of vertical position if (boardX >= 0 && boardX < gameBoard.width) { - drawBlock(canvas, boardX, boardY, true, false) + val screenX = boardLeft + boardX * blockSize + val screenY = boardTop + boardY * blockSize + + // Draw background + canvas.drawRect( + screenX + 1f, + screenY + 1f, + screenX + blockSize - 1f, + screenY + blockSize - 1f, + ghostBackgroundPaint + ) + + // Draw border + canvas.drawRect( + screenX + 1f, + screenY + 1f, + screenX + blockSize - 1f, + screenY + blockSize - 1f, + ghostBorderPaint + ) + + // Draw outline + canvas.drawRect( + screenX + 1f, + screenY + 1f, + screenX + blockSize - 1f, + screenY + blockSize - 1f, + ghostPaint + ) } } } From ce19427ccaa9250f8b3520a34e322defbd2d09dd Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sun, 30 Mar 2025 18:15:07 -0400 Subject: [PATCH 3/6] fix: hold piece now correctly uses next piece preview on first hold --- app/src/main/java/com/mintris/model/GameBoard.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index ce26763..8d3c700 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -97,8 +97,13 @@ class GameBoard( if (holdPiece == null) { // If no piece is held, hold current piece and spawn new one holdPiece = current + currentPiece = nextPiece spawnNextPiece() - spawnPiece() + // Reset position of new piece + currentPiece?.apply { + x = (width - getWidth()) / 2 + y = 0 + } } else { // Swap current piece with held piece currentPiece = holdPiece From 292ea656f8ed893b5cd3b2ca3109f78c76037f77 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sun, 30 Mar 2025 19:02:27 -0400 Subject: [PATCH 4/6] Enhanced game over experience: Added louder game over sound, improved game over animation, and added haptic feedback --- app/src/main/java/com/mintris/MainActivity.kt | 6 ++ .../main/java/com/mintris/audio/GameMusic.kt | 47 ++++++++++++++++ .../main/java/com/mintris/game/GameHaptics.kt | 22 ++++++++ .../main/java/com/mintris/game/GameView.kt | 52 ++++++++++++++++++ .../main/java/com/mintris/model/GameBoard.kt | 9 ++- app/src/main/res/raw/game_over.mp3 | Bin 0 -> 8256 bytes 6 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/raw/game_over.mp3 diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 8b4602e..abfc24c 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -366,6 +366,12 @@ class MainActivity : AppCompatActivity() { // Flag to track if high score screen will be shown var showingHighScore = false + // Play game over sound and trigger animation + if (isSoundEnabled) { + gameMusic.playGameOver() + } + gameView.startGameOverAnimation() + // Show progression screen first with XP animation showProgressionScreen(xpGained, newRewards) diff --git a/app/src/main/java/com/mintris/audio/GameMusic.kt b/app/src/main/java/com/mintris/audio/GameMusic.kt index fbb8abf..32c2f6e 100644 --- a/app/src/main/java/com/mintris/audio/GameMusic.kt +++ b/app/src/main/java/com/mintris/audio/GameMusic.kt @@ -9,12 +9,14 @@ import com.mintris.R class GameMusic(private val context: Context) { private var mediaPlayer: MediaPlayer? = null + private var gameOverPlayer: MediaPlayer? = null private var isEnabled = true private var isPrepared = false init { try { setupMediaPlayer() + setupGameOverPlayer() } catch (e: Exception) { Log.e("GameMusic", "Error initializing: ${e.message}") } @@ -46,6 +48,49 @@ class GameMusic(private val context: Context) { } } + private fun setupGameOverPlayer() { + try { + Log.d("GameMusic", "Setting up GameOver MediaPlayer") + gameOverPlayer = MediaPlayer.create(context, R.raw.game_over).apply { + setVolume(1.0f, 1.0f) // Increased from 0.7f to 1.0f for maximum volume + + // Set audio attributes for better performance + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) + } + } + Log.d("GameMusic", "GameOver MediaPlayer setup complete") + } catch (e: Exception) { + Log.e("GameMusic", "Error setting up GameOver MediaPlayer", e) + gameOverPlayer = null + } + } + + fun playGameOver() { + if (isEnabled && gameOverPlayer != null) { + try { + Log.d("GameMusic", "Playing game over sound") + // Temporarily lower background music volume + mediaPlayer?.setVolume(0.2f, 0.2f) + + // Play game over sound + gameOverPlayer?.start() + + // Restore background music volume after a delay + gameOverPlayer?.setOnCompletionListener { + mediaPlayer?.setVolume(0.5f, 0.5f) + } + } catch (e: Exception) { + Log.e("GameMusic", "Error playing game over sound: ${e.message}") + } + } + } + fun start() { if (isEnabled && mediaPlayer != null && isPrepared) { try { @@ -107,7 +152,9 @@ class GameMusic(private val context: Context) { try { Log.d("GameMusic", "Releasing MediaPlayer") mediaPlayer?.release() + gameOverPlayer?.release() mediaPlayer = null + gameOverPlayer = null isPrepared = false } catch (e: Exception) { Log.e("GameMusic", "Error releasing music: ${e.message}") diff --git a/app/src/main/java/com/mintris/game/GameHaptics.kt b/app/src/main/java/com/mintris/game/GameHaptics.kt index 44e5011..d6869c0 100644 --- a/app/src/main/java/com/mintris/game/GameHaptics.kt +++ b/app/src/main/java/com/mintris/game/GameHaptics.kt @@ -91,4 +91,26 @@ class GameHaptics(private val context: Context) { vibrator.vibrate(vibrationEffect) } } + + fun vibrateForGameOver() { + Log.d(TAG, "Attempting to vibrate for game over") + + // Only proceed if the device has a vibrator and it's available + if (!vibrator.hasVibrator()) return + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create a strong, long vibration effect + val vibrationEffect = VibrationEffect.createOneShot(300L, VibrationEffect.DEFAULT_AMPLITUDE) + vibrator.vibrate(vibrationEffect) + Log.d(TAG, "Game over vibration triggered successfully") + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(300L) + Log.w(TAG, "Device does not support vibration effects (Android < 8.0)") + } + } catch (e: Exception) { + Log.e(TAG, "Error triggering game over vibration", e) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 27e38a0..85a44a4 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -178,6 +178,9 @@ class GameView @JvmOverloads constructor( private var pulseAlpha = 0f private var isPulsing = false private var linesToPulse = mutableListOf() // Track which lines are being cleared + private var gameOverAnimator: ValueAnimator? = null + private var gameOverAlpha = 0f + private var isGameOverAnimating = false private val ghostPaint = Paint().apply { style = Paint.Style.STROKE @@ -439,6 +442,18 @@ class GameView @JvmOverloads constructor( // Draw active piece drawActivePiece(canvas) } + + // Draw game over effect if animating + if (isGameOverAnimating) { + val gameOverPaint = Paint().apply { + color = Color.WHITE + alpha = (128 * gameOverAlpha).toInt() + isAntiAlias = true + style = Paint.Style.FILL + maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) + } + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint) + } } /** @@ -1133,4 +1148,41 @@ class GameView @JvmOverloads constructor( super.setBackgroundColor(color) invalidate() } + + /** + * Start the game over animation + */ + fun startGameOverAnimation() { + Log.d(TAG, "Starting game over animation") + + // Cancel any existing animations + pulseAnimator?.cancel() + gameOverAnimator?.cancel() + + // Trigger haptic feedback + gameHaptics?.vibrateForGameOver() + + // Create new game over animation + gameOverAnimator = ValueAnimator.ofFloat(0f, 1f, 0.7f).apply { + duration = 1500L // 1.5 seconds total + interpolator = LinearInterpolator() + + addUpdateListener { animation -> + gameOverAlpha = animation.animatedValue as Float + isGameOverAnimating = true + invalidate() + Log.d(TAG, "Game over animation update: alpha = $gameOverAlpha") + } + + addListener(object : android.animation.AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: android.animation.Animator) { + isGameOverAnimating = false + gameOverAlpha = 0.7f // Keep at 70% opacity + invalidate() + Log.d(TAG, "Game over animation ended") + } + }) + } + gameOverAnimator?.start() + } } diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index 8d3c700..31f03aa 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -136,10 +136,10 @@ class GameBoard( currentPiece = nextPiece spawnNextPiece() - // Center the piece horizontally + // Center the piece horizontally and spawn one unit higher currentPiece?.apply { x = (width - getWidth()) / 2 - y = 0 + y = -1 // Spawn one unit above the top of the screen Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}") @@ -326,6 +326,11 @@ class GameBoard( if (boardY >= 0 && grid[boardY][boardX]) { return false } + + // Check if the position is more than one unit above the top of the screen + if (boardY < -1) { + return false + } } } } diff --git a/app/src/main/res/raw/game_over.mp3 b/app/src/main/res/raw/game_over.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d1892819e7cd85843066a3c971c961d721098a33 GIT binary patch literal 8256 zcmchcXH-+$w#RpR0ue%w^d=xu5{jZCB!CnR9aN-+jvPf0`w>Y9V89?Ks8K;siXf;c zDuM?|Xi}npprWE8Akq{BL&m1puDGl|4|s~U1-}gJ@$3}S1=9s^%)W$jcG%v-Gwt5JUWRe1;A=yyL{{p z55Y56S3OHq{xLi3uU()pNbr!cQ9L<7dYkfXKgV=vyNh&4a~PQAIT0RYN(8^}tvlsN zxiOP%u>zOGvh#r60pl`&zJb|K-_HUeepIr=w_lWB&oW@O|)ri7p`xyOU zTI9pnJz#_IEeJ(rgr;l?SBBxol^o;{B^gUxnae&ztDLb51eXs)Dr|y_9_&`WzP90P zdT!o9W{;%I>c;cahf;&tDjAmf>E(For|A+( z#Qa#6)6`~V(~pi!)r>Hgu0Ch{uc!m=U#T&iEdX7h8mDrio6*F@*XDI>fR) z%~XZwUrc~V@2rXP`VX0_Twm{MLlC1leL3M(|A z@cCty!JtmJWrwX{LQHVQo;%0Zd;xb~NdgtX22kj$3LtqNFe0x4{)2x6H`rqS6e~oc z?=`+f;Q8{f!IAB>c|oTXM>lUJ-Fg>I^)L!}#eTEnQMT=>{PYFy`}0ZeQJ~NkT%hor zfHJOvH*!SGzY^hs^i-6F^vAJ>QKCF9a!%sjavf8(sHP!6B4@GLI}BAsc1njE5Vtd!^5sKOc4uF@H7q4mDx225#JZBDvS>%0A<#-?!wx4w_g6f;C{Ts9-*u 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

_o)_O33G`mQq6YD_)qwMPsUyvx{85Ux_Lyv z>%u(BYdj{98LY$Os{CAkr9T;RB|8h)_(~+3S-uc3rk9CHzIkYc^{It1P^j_q_U$s5 zd!g|uqMbtrWMj%t8HV;F<3iRZZ`t2MuJedIMO|)Cw)YuU>#U&t0jt5Ub@R*ZL!0s) zH~;K?c0-H*r~f0^4rHVoNT;f$tMj4$?d|3NjGwfGbRy?&N?WaKbjD`E$;S)<2T9MU z#Wp$NLn{zY03L886;AQBR#;wr$&0DVe0e-p{rq^lg11+VOl`^DDeytVYYM80ssefMWdsajjQf}tZkayS zUYa9KK!zy6ZS@A6@-L3jhN;TRSy4IWUtYXnZ}ED}7G1ltTVLm@=gH~nyD5n${WI)V zUUhIVXsft*OSVrY;g)5-!suo-`u$!r3?U;VYOUU~{&2J;bw#N+^bDmwo^WOL8^@g{ zqpR~Ar3iZa`Rd4GV@XB!@QZ8HQUU7D=wtBB0BT&U{{#z*v=}v=a^S;LRj8tH_U#1E z=(>p=Mj0u23bkX;`Z#^h`}xTg&#O*93T#$Mu)Dpo!^H5FOHd*JL<0a4Z#7Ze!`DbI zZHiYXlPKT(p}aPKcyGJZVtoV5SUX+6sv?i(x$fBZc>v+URK;zRUq7t*Iv1hrVBTcE zk+vlbF1sn2jy0-}ZhTn0cG~Q>4(rt2>qi344SYJaYS+N5{-ZXqrr~8=J*`w!KWFGN zUv6yOX+L-UjH*=(Ffw4LQt z{fW7@a~m!IzBy=*0MeMOx$l`QjMMmdD&p$Klj&)MYfQeJNk>}39s zG3x`;k4(LPkX~0_r&}Ib=e9Lq>b+UrNefpMvfQ0`3F^A4mxttu`)Yq|>PWhkaaef2 zplT(6R!EPs@+sBgM1I6)Hs)npWeGzVA2i)^%)HHF$%r-tk7T8dluHAC{(k|oq%vtd z_nD2pq7i3l25+b+y=vEH=Zf~537;-oygT`{)QjIR8OqqN^&LI0Uon3b@U7N% z(oC}{VAZ-2&&h87(e!lUeUfrQk(8|m{+4>Wd(-6{oIKY#7oJk++=o*=C!3?a<#McU zkZ#b5Eys7>5%9G?YroUECU-~f+VYglj)R-7`$yi5G7K#>yc;umJ#yu2sbP)bXw8bd z+e>9D&fkui4K2AF7Be&Hm@gNi`G+! z_Kl>IXBtI;cr!-7!U^PTBe|G;-qS+K(jq=2o8TPa7*4M$i|A0&jx;ii=-I#SlFwvR zRB$!;_pRSw^&tQCZ({z>EWC{J1NWVF?Ka2!Py@tm|Mh79{}+n+Ke1%Ak_E1VO^nH7 Qp1g(S*#Ccw|N4&q0~0C_!~g&Q literal 0 HcmV?d00001 From 7dccad8d12811b20d63253083588431805223e37 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sun, 30 Mar 2025 19:14:16 -0400 Subject: [PATCH 5/6] fix: prevent crash in game over animation by checking for zero blur radius --- .../main/java/com/mintris/game/GameView.kt | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 85a44a4..741b662 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -57,6 +57,15 @@ class GameView @JvmOverloads constructor( isAntiAlias = true } + private val borderGlowPaint = Paint().apply { + color = Color.WHITE + alpha = 60 + isAntiAlias = true + style = Paint.Style.STROKE + strokeWidth = 2f + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) + } + private val ghostBlockPaint = Paint().apply { color = Color.WHITE alpha = 80 // 30% opacity @@ -89,15 +98,6 @@ class GameView @JvmOverloads constructor( maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) } - private val borderGlowPaint = Paint().apply { - color = Color.WHITE - alpha = 60 - isAntiAlias = true - style = Paint.Style.STROKE - strokeWidth = 2f - maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) - } - // Add a new paint for the pulse effect private val pulsePaint = Paint().apply { color = Color.CYAN @@ -410,12 +410,6 @@ class GameView @JvmOverloads constructor( } override fun onDraw(canvas: Canvas) { - // Skip drawing if paused or game over - faster return - if (isPaused || gameBoard.isGameOver) { - super.onDraw(canvas) - return - } - // Set hardware layer type during draw for better performance val wasHardwareAccelerated = isHardwareAccelerated if (!wasHardwareAccelerated) { @@ -447,12 +441,28 @@ class GameView @JvmOverloads constructor( if (isGameOverAnimating) { val gameOverPaint = Paint().apply { color = Color.WHITE - alpha = (128 * gameOverAlpha).toInt() + alpha = (200 * gameOverAlpha).toInt() // Increased from 128 to 200 isAntiAlias = true style = Paint.Style.FILL - maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) + // Only apply blur if alpha is greater than 0 + if (gameOverAlpha > 0) { + maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased from 48f to 64f + } } canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint) + + // Add a second layer for more intensity + val gameOverPaint2 = Paint().apply { + color = Color.WHITE + alpha = (100 * gameOverAlpha).toInt() + 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) + } + } + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint2) } } @@ -1163,8 +1173,8 @@ class GameView @JvmOverloads constructor( gameHaptics?.vibrateForGameOver() // Create new game over animation - gameOverAnimator = ValueAnimator.ofFloat(0f, 1f, 0.7f).apply { - duration = 1500L // 1.5 seconds total + gameOverAnimator = ValueAnimator.ofFloat(0f, 1f, 0.8f).apply { + duration = 1000L // 1 second total interpolator = LinearInterpolator() addUpdateListener { animation -> @@ -1177,7 +1187,7 @@ class GameView @JvmOverloads constructor( addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { isGameOverAnimating = false - gameOverAlpha = 0.7f // Keep at 70% opacity + gameOverAlpha = 0.8f // Keep at 80% opacity invalidate() Log.d(TAG, "Game over animation ended") } From 94e8d313c2e1cda53f2bfe0d896d52c012079039 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sun, 30 Mar 2025 19:39:35 -0400 Subject: [PATCH 6/6] Add enhanced game over animation with falling blocks, screen shake, and dynamic text effects --- app/src/main/java/com/mintris/MainActivity.kt | 49 ++-- .../main/java/com/mintris/game/GameView.kt | 256 ++++++++++++++++-- 2 files changed, 268 insertions(+), 37 deletions(-) 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 + } + } }