From 9ab9b534071386da15506105f7bfd4810edbdcb5 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Fri, 28 Mar 2025 12:33:42 -0400 Subject: [PATCH 01/57] Fix menu shifting issue and code cleanup. Fix menu shifting by adding fillViewport and padding to ScrollView. Added missing getLastClearedLines method. Improved code quality with proper logging constants and removed unused imports. --- app/src/main/java/com/mintris/MainActivity.kt | 25 +++--- .../main/java/com/mintris/game/GameHaptics.kt | 87 +++++++++++-------- .../main/java/com/mintris/game/GameView.kt | 38 ++++---- .../main/java/com/mintris/model/GameBoard.kt | 36 ++++++-- app/src/main/res/layout/activity_main.xml | 6 +- 5 files changed, 119 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 2f39436..95ef64c 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -1,26 +1,18 @@ package com.mintris -import android.animation.ObjectAnimator -import android.animation.ValueAnimator import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import android.os.VibrationEffect import android.os.Vibrator -import android.os.VibratorManager import android.view.View -import android.widget.Button -import android.widget.LinearLayout -import android.widget.TextView +import android.view.HapticFeedbackConstants import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import com.mintris.databinding.ActivityMainBinding import com.mintris.game.GameHaptics import com.mintris.game.GameView -import com.mintris.game.NextPieceView import com.mintris.game.TitleScreen -import android.view.HapticFeedbackConstants import com.mintris.model.GameBoard import com.mintris.audio.GameMusic import com.mintris.model.HighScoreManager @@ -33,10 +25,15 @@ import android.graphics.Color import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import android.graphics.Rect +import android.util.Log import android.view.KeyEvent class MainActivity : AppCompatActivity() { + companion object { + private const val TAG = "MainActivity" + } + // UI components private lateinit var binding: ActivityMainBinding private lateinit var gameView: GameView @@ -165,18 +162,18 @@ class MainActivity : AppCompatActivity() { } gameView.onLineClear = { lineCount -> - android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines") + Log.d(TAG, "Received line clear callback: $lineCount lines") // Use enhanced haptic feedback for line clears if (isSoundEnabled) { - android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback") + Log.d(TAG, "Sound is enabled, triggering haptic feedback") try { gameHaptics.vibrateForLineClear(lineCount) - android.util.Log.d("MainActivity", "Haptic feedback triggered successfully") + Log.d(TAG, "Haptic feedback triggered successfully") } catch (e: Exception) { - android.util.Log.e("MainActivity", "Error triggering haptic feedback", e) + Log.e(TAG, "Error triggering haptic feedback", e) } } else { - android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback") + Log.d(TAG, "Sound is disabled, skipping haptic feedback") } // Record line clear in stats statsManager.recordLineClear(lineCount) diff --git a/app/src/main/java/com/mintris/game/GameHaptics.kt b/app/src/main/java/com/mintris/game/GameHaptics.kt index 9dc1e87..44e5011 100644 --- a/app/src/main/java/com/mintris/game/GameHaptics.kt +++ b/app/src/main/java/com/mintris/game/GameHaptics.kt @@ -7,15 +7,66 @@ import android.os.Vibrator import android.os.VibratorManager import android.view.HapticFeedbackConstants import android.view.View +import android.util.Log +/** + * Handles haptic feedback for game events + */ class GameHaptics(private val context: Context) { - private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + + companion object { + private const val TAG = "GameHaptics" + } + + // Vibrator service + private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator } + + // Vibrate for line clear (more intense for more lines) + fun vibrateForLineClear(lineCount: Int) { + Log.d(TAG, "Attempting to vibrate for $lineCount lines") + + // Only proceed if the device has a vibrator and it's available + if (!vibrator.hasVibrator()) return + + // Scale duration and amplitude based on line count + // More lines = longer and stronger vibration + val duration = when(lineCount) { + 1 -> 50L // Single line: short vibration + 2 -> 80L // Double line: slightly longer + 3 -> 120L // Triple line: even longer + 4 -> 200L // Tetris: longest vibration + else -> 50L + } + + val amplitude = when(lineCount) { + 1 -> 80 // Single line: mild vibration (80/255) + 2 -> 120 // Double line: medium vibration (120/255) + 3 -> 180 // Triple line: strong vibration (180/255) + 4 -> 255 // Tetris: maximum vibration (255/255) + else -> 80 + } + + Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude") + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude)) + Log.d(TAG, "Vibration triggered successfully") + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(duration) + Log.w(TAG, "Device does not support vibration effects (Android < 8.0)") + } + } catch (e: Exception) { + Log.e(TAG, "Error triggering vibration", e) + } + } fun performHapticFeedback(view: View, feedbackType: Int) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -26,40 +77,6 @@ class GameHaptics(private val context: Context) { } } - fun vibrateForLineClear(lineCount: Int) { - android.util.Log.d("GameHaptics", "Attempting to vibrate for $lineCount lines") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val duration = when (lineCount) { - 4 -> 200L // Tetris - doubled from 100L - 3 -> 160L // Triples - doubled from 80L - 2 -> 120L // Doubles - doubled from 60L - 1 -> 80L // Singles - doubled from 40L - else -> 0L - } - - val amplitude = when (lineCount) { - 4 -> 255 // Full amplitude for Tetris - 3 -> 230 // 90% amplitude for triples - 2 -> 180 // 70% amplitude for doubles - 1 -> 128 // 50% amplitude for singles - else -> 0 - } - - android.util.Log.d("GameHaptics", "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude") - if (duration > 0 && amplitude > 0) { - try { - val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude) - vibrator.vibrate(vibrationEffect) - android.util.Log.d("GameHaptics", "Vibration triggered successfully") - } catch (e: Exception) { - android.util.Log.e("GameHaptics", "Error triggering vibration", e) - } - } - } else { - android.util.Log.w("GameHaptics", "Device does not support vibration effects (Android < 8.0)") - } - } - fun vibrateForPieceLock() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 95332db..95d4822 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -12,12 +12,12 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet +import android.util.Log import android.view.MotionEvent import android.view.View import android.view.animation.LinearInterpolator -import android.view.WindowManager -import android.view.Display import android.hardware.display.DisplayManager +import android.view.Display import com.mintris.model.GameBoard import com.mintris.model.Tetromino import com.mintris.model.TetrominoType @@ -33,6 +33,10 @@ class GameView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + companion object { + private const val TAG = "GameView" + } + // Game board model private var gameBoard = GameBoard() private var gameHaptics: GameHaptics? = null @@ -165,17 +169,17 @@ class GameView @JvmOverloads constructor( gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onLineClear = { lineCount, clearedLines -> - android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") + Log.d(TAG, "Received line clear from GameBoard: $lineCount lines") try { onLineClear?.invoke(lineCount) // Use the lines that were cleared directly linesToPulse.clear() linesToPulse.addAll(clearedLines) - android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") + Log.d(TAG, "Found ${linesToPulse.size} lines to pulse") startPulseAnimation(lineCount) - android.util.Log.d("GameView", "Forwarded line clear callback") + Log.d(TAG, "Forwarded line clear callback") } catch (e: Exception) { - android.util.Log.e("GameView", "Error forwarding line clear callback", e) + Log.e(TAG, "Error forwarding line clear callback", e) } } @@ -184,7 +188,11 @@ class GameView @JvmOverloads constructor( // Set better frame rate using modern APIs val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) + val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + displayManager.getDisplay(Display.DEFAULT_DISPLAY) + } else { + displayManager.displays.firstOrNull() + } display?.let { disp -> val refreshRate = disp.refreshRate // Set game loop interval based on refresh rate, but don't go faster than the base interval @@ -289,7 +297,7 @@ class GameView @JvmOverloads constructor( val totalHeight = blockSize * verticalBlocks // Log dimensions for debugging - android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") + Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") } override fun onDraw(canvas: Canvas) { @@ -739,17 +747,17 @@ class GameView @JvmOverloads constructor( gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onLineClear = { lineCount, clearedLines -> - android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") + Log.d(TAG, "Received line clear from GameBoard: $lineCount lines") try { onLineClear?.invoke(lineCount) // Use the lines that were cleared directly linesToPulse.clear() linesToPulse.addAll(clearedLines) - android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") + Log.d(TAG, "Found ${linesToPulse.size} lines to pulse") startPulseAnimation(lineCount) - android.util.Log.d("GameView", "Forwarded line clear callback") + Log.d(TAG, "Forwarded line clear callback") } catch (e: Exception) { - android.util.Log.e("GameView", "Error forwarding line clear callback", e) + Log.e(TAG, "Error forwarding line clear callback", e) } } @@ -785,7 +793,7 @@ class GameView @JvmOverloads constructor( * Start the pulse animation for line clear */ private fun startPulseAnimation(lineCount: Int) { - android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines") + Log.d(TAG, "Starting pulse animation for $lineCount lines") // Cancel any existing animation pulseAnimator?.cancel() @@ -804,7 +812,7 @@ class GameView @JvmOverloads constructor( pulseAlpha = animation.animatedValue as Float isPulsing = true invalidate() - android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha") + Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha") } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { @@ -812,7 +820,7 @@ class GameView @JvmOverloads constructor( pulseAlpha = 0f linesToPulse.clear() invalidate() - android.util.Log.d("GameView", "Pulse animation ended") + Log.d(TAG, "Pulse animation ended") } }) } diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index 5203109..b72921b 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -1,6 +1,6 @@ package com.mintris.model -import kotlin.random.Random +import android.util.Log /** * Represents the game board (grid) and manages game state @@ -9,6 +9,10 @@ class GameBoard( val width: Int = 10, val height: Int = 20 ) { + companion object { + private const val TAG = "GameBoard" + } + // Board grid to track locked pieces // True = occupied, False = empty private val grid = Array(height) { BooleanArray(width) { false } } @@ -55,6 +59,9 @@ class GameBoard( var onNextPieceChanged: (() -> Unit)? = null var onLineClear: ((Int, List) -> Unit)? = null + // Store the last cleared lines + private val lastClearedLines = mutableListOf() + init { spawnNextPiece() spawnPiece() @@ -66,7 +73,7 @@ class GameBoard( private fun spawnNextPiece() { // If bag is empty, refill it with all piece types if (bag.isEmpty()) { - bag.addAll(TetrominoType.values()) + bag.addAll(TetrominoType.entries.toTypedArray()) bag.shuffle() } @@ -326,18 +333,26 @@ class GameBoard( y-- } + // Store the last cleared lines + lastClearedLines.clear() + lastClearedLines.addAll(linesToClear) + // If lines were cleared, calculate score in background and trigger callback if (shiftAmount > 0) { - android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount") + // Log line clear + Log.d(TAG, "Lines cleared: $shiftAmount") + // Trigger line clear callback on main thread with the lines that were cleared val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) mainHandler.post { - android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines") + // Call the line clear callback with the cleared line count try { - onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared - android.util.Log.d("GameBoard", "onLineClear callback completed successfully") + Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines") + val clearedLines = getLastClearedLines() + onLineClear?.invoke(shiftAmount, clearedLines) + Log.d(TAG, "onLineClear callback completed successfully") } catch (e: Exception) { - android.util.Log.e("GameBoard", "Error in onLineClear callback", e) + Log.e(TAG, "Error in onLineClear callback", e) } } @@ -582,4 +597,11 @@ class GameBoard( * Get the current combo count */ fun getCombo(): Int = combo + + /** + * Get the list of lines that were most recently cleared + */ + private fun getLastClearedLines(): List { + return lastClearedLines.toList() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 371cb8c..939dc48 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -282,13 +282,15 @@ + android:layout_weight="1" + android:fillViewport="true"> + android:gravity="center" + android:paddingTop="16dp">