From 809ae33e5ecaf921901f1ae445195e445fd6d984 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Fri, 28 Mar 2025 09:11:59 -0400 Subject: [PATCH 01/59] Enhanced theme system with immediate UI updates, improved progression speed, and added visual/haptic feedback for theme selection --- .../com/mintris/HighScoreEntryActivity.kt | 72 +++++++++--- .../java/com/mintris/HighScoresActivity.kt | 108 +++++++++++++++--- app/src/main/java/com/mintris/MainActivity.kt | 45 ++++++++ .../main/java/com/mintris/StatsActivity.kt | 57 +++++++++ .../com/mintris/model/HighScoreAdapter.kt | 31 ++++- .../mintris/model/PlayerProgressionManager.kt | 14 +-- .../main/java/com/mintris/ui/ThemeSelector.kt | 32 +++++- app/src/main/res/layout/activity_main.xml | 3 + 8 files changed, 316 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/mintris/HighScoreEntryActivity.kt b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt index 313685b..8829d52 100644 --- a/app/src/main/java/com/mintris/HighScoreEntryActivity.kt +++ b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt @@ -5,39 +5,46 @@ import android.os.Bundle import android.widget.Button import android.widget.EditText import android.widget.TextView +import android.view.View import androidx.appcompat.app.AppCompatActivity +import com.mintris.databinding.HighScoreEntryBinding import com.mintris.model.HighScore import com.mintris.model.HighScoreManager +import com.mintris.model.PlayerProgressionManager +import android.graphics.Color class HighScoreEntryActivity : AppCompatActivity() { + private lateinit var binding: HighScoreEntryBinding private lateinit var highScoreManager: HighScoreManager - private lateinit var nameInput: EditText - private lateinit var scoreText: TextView - private lateinit var saveButton: Button + private lateinit var progressionManager: PlayerProgressionManager + private var currentTheme = PlayerProgressionManager.THEME_CLASSIC + private var score: Int = 0 // Track if we already saved to prevent double-saving private var hasSaved = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.high_score_entry) + binding = HighScoreEntryBinding.inflate(layoutInflater) + setContentView(binding.root) highScoreManager = HighScoreManager(this) - nameInput = findViewById(R.id.nameInput) - scoreText = findViewById(R.id.scoreText) - saveButton = findViewById(R.id.saveButton) + progressionManager = PlayerProgressionManager(this) + + // Load and apply theme + currentTheme = loadThemePreference() + applyTheme(currentTheme) - val score = intent.getIntExtra("score", 0) - val level = intent.getIntExtra("level", 1) - scoreText.text = getString(R.string.score) + ": $score" + score = intent.getIntExtra("score", 0) + binding.scoreText.text = "Score: $score" - saveButton.setOnClickListener { + binding.saveButton.setOnClickListener { // Only allow saving once if (!hasSaved) { - val name = nameInput.text.toString().trim() + val name = binding.nameInput.text.toString().trim() if (name.isNotEmpty()) { hasSaved = true - val highScore = HighScore(name, score, level) + val highScore = HighScore(name, score, 1) highScoreManager.addHighScore(highScore) // Set result and finish @@ -50,10 +57,49 @@ class HighScoreEntryActivity : AppCompatActivity() { // Prevent accidental back button press from causing issues override fun onBackPressed() { + super.onBackPressed() // If they haven't saved yet, consider it a cancel if (!hasSaved) { setResult(Activity.RESULT_CANCELED) } finish() } + + private fun loadThemePreference(): String { + val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE) + return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC + } + + private fun applyTheme(themeId: String) { + // Set background color + val backgroundColor = when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221") + PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A") + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832") + PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10") + else -> Color.BLACK + } + binding.root.setBackgroundColor(backgroundColor) + + // Set text color + val textColor = when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF") + PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F") + PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1") + else -> Color.WHITE + } + + // Apply text color to score and input + binding.scoreText.setTextColor(textColor) + binding.nameInput.setTextColor(textColor) + binding.nameInput.setHintTextColor(Color.argb(128, Color.red(textColor), Color.green(textColor), Color.blue(textColor))) + + // Apply theme to submit button + binding.saveButton.setTextColor(textColor) + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/HighScoresActivity.kt b/app/src/main/java/com/mintris/HighScoresActivity.kt index da6dd9c..ae525bf 100644 --- a/app/src/main/java/com/mintris/HighScoresActivity.kt +++ b/app/src/main/java/com/mintris/HighScoresActivity.kt @@ -2,44 +2,116 @@ package com.mintris import android.os.Bundle import android.widget.Button +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.mintris.databinding.HighScoresBinding import com.mintris.model.HighScoreAdapter import com.mintris.model.HighScoreManager +import com.mintris.model.PlayerProgressionManager +import android.graphics.Color +import android.util.Log class HighScoresActivity : AppCompatActivity() { + private lateinit var binding: HighScoresBinding private lateinit var highScoreManager: HighScoreManager private lateinit var highScoreAdapter: HighScoreAdapter - private lateinit var highScoresList: RecyclerView - private lateinit var backButton: Button + private lateinit var progressionManager: PlayerProgressionManager + private var currentTheme = PlayerProgressionManager.THEME_CLASSIC override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.high_scores) - - highScoreManager = HighScoreManager(this) - highScoresList = findViewById(R.id.highScoresList) - backButton = findViewById(R.id.backButton) - - highScoreAdapter = HighScoreAdapter() - highScoresList.layoutManager = LinearLayoutManager(this) - highScoresList.adapter = highScoreAdapter - - updateHighScores() - - backButton.setOnClickListener { + + try { + binding = HighScoresBinding.inflate(layoutInflater) + setContentView(binding.root) + + highScoreManager = HighScoreManager(this) + progressionManager = PlayerProgressionManager(this) + + // Load and apply theme + currentTheme = loadThemePreference() + + // Initialize adapter before applying theme + highScoreAdapter = HighScoreAdapter() + + // Set up RecyclerView + binding.highScoresList.layoutManager = LinearLayoutManager(this) + binding.highScoresList.adapter = highScoreAdapter + + // Now apply theme to UI + applyTheme(currentTheme) + + // Load high scores + updateHighScores() + + // Set up back button + binding.backButton.setOnClickListener { + finish() + } + } catch (e: Exception) { + Log.e("HighScoresActivity", "Error in onCreate", e) + // Show an error message if necessary, or finish gracefully finish() } } + private fun loadThemePreference(): String { + val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE) + return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC + } + + private fun applyTheme(themeId: String) { + try { + // Set background color + val backgroundColor = when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221") + PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A") + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832") + PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10") + else -> Color.BLACK + } + binding.root.setBackgroundColor(backgroundColor) + + // Set text color + val textColor = when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF") + PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F") + PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1") + else -> Color.WHITE + } + + // Apply theme to back button + binding.backButton.setTextColor(textColor) + + // Update adapter theme + highScoreAdapter.applyTheme(themeId) + } catch (e: Exception) { + Log.e("HighScoresActivity", "Error applying theme: $themeId", e) + } + } + private fun updateHighScores() { - val scores = highScoreManager.getHighScores() - highScoreAdapter.updateHighScores(scores) + try { + val scores = highScoreManager.getHighScores() + highScoreAdapter.updateHighScores(scores) + } catch (e: Exception) { + Log.e("HighScoresActivity", "Error updating high scores", e) + } } override fun onResume() { super.onResume() - updateHighScores() + try { + updateHighScores() + } catch (e: Exception) { + Log.e("HighScoresActivity", "Error in onResume", e) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 83fdd6c..e403d6c 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -102,7 +102,16 @@ class MainActivity : AppCompatActivity() { // Set up theme selector val themeSelector = binding.themeSelector themeSelector.onThemeSelected = { themeId -> + // Apply the new theme applyTheme(themeId) + + // Provide haptic feedback as a cue that the theme changed + gameHaptics.vibrateForPieceLock() + + // Refresh the pause menu to immediately show theme changes + if (binding.pauseContainer.visibility == View.VISIBLE) { + showPauseMenu() + } } // Set up title screen @@ -381,6 +390,38 @@ class MainActivity : AppCompatActivity() { binding.pauseLevelBadge.setLevel(progressionManager.getPlayerLevel()) binding.pauseLevelBadge.setThemeColor(getThemeColor(currentTheme)) + // Get theme color + val textColor = getThemeColor(currentTheme) + + // Apply theme color to pause container background + val backgroundColor = when (currentTheme) { + PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221") + PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A") + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832") + PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10") + else -> Color.BLACK + } + binding.pauseContainer.setBackgroundColor(backgroundColor) + + // Apply theme colors to buttons + binding.pauseStartButton.setTextColor(textColor) + binding.pauseRestartButton.setTextColor(textColor) + binding.resumeButton.setTextColor(textColor) + binding.highScoresButton.setTextColor(textColor) + binding.statsButton.setTextColor(textColor) + binding.pauseLevelText.setTextColor(textColor) + binding.pauseLevelUpButton.setTextColor(textColor) + binding.pauseLevelDownButton.setTextColor(textColor) + binding.settingsButton.setTextColor(textColor) + binding.musicToggle.setColorFilter(textColor) + + // Apply theme colors to text elements + binding.settingsTitle.setTextColor(textColor) + binding.selectLevelText.setTextColor(textColor) + binding.musicText.setTextColor(textColor) + // Update theme selector updateThemeSelector() } @@ -567,6 +608,10 @@ class MainActivity : AppCompatActivity() { progressionScreen.applyTheme(themeId) } + // Apply theme color to the stats button + val textColor = getThemeColor(currentTheme) + binding.statsButton.setTextColor(textColor) + // Update the game view to apply theme gameView.invalidate() } diff --git a/app/src/main/java/com/mintris/StatsActivity.kt b/app/src/main/java/com/mintris/StatsActivity.kt index edcc765..e7f6f3e 100644 --- a/app/src/main/java/com/mintris/StatsActivity.kt +++ b/app/src/main/java/com/mintris/StatsActivity.kt @@ -7,12 +7,16 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.mintris.databinding.ActivityStatsBinding import com.mintris.model.StatsManager +import com.mintris.model.PlayerProgressionManager +import android.graphics.Color import java.text.SimpleDateFormat import java.util.* class StatsActivity : AppCompatActivity() { private lateinit var binding: ActivityStatsBinding private lateinit var statsManager: StatsManager + private lateinit var progressionManager: PlayerProgressionManager + private var currentTheme = PlayerProgressionManager.THEME_CLASSIC override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -20,6 +24,11 @@ class StatsActivity : AppCompatActivity() { setContentView(binding.root) statsManager = StatsManager(this) + progressionManager = PlayerProgressionManager(this) + + // Load and apply theme + currentTheme = loadThemePreference() + applyTheme(currentTheme) // Set up back button binding.backButton.setOnClickListener { @@ -34,6 +43,54 @@ class StatsActivity : AppCompatActivity() { updateStats() } + private fun loadThemePreference(): String { + val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE) + return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC + } + + private fun applyTheme(themeId: String) { + // Set background color + val backgroundColor = when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221") + PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A") + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832") + PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10") + else -> Color.BLACK + } + binding.root.setBackgroundColor(backgroundColor) + + // Set text color + val textColor = when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE + PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF") + PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY + PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F") + PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK + PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1") + else -> Color.WHITE + } + + // Apply text color to all TextViews + binding.totalGamesText.setTextColor(textColor) + binding.totalScoreText.setTextColor(textColor) + binding.totalLinesText.setTextColor(textColor) + binding.totalPiecesText.setTextColor(textColor) + binding.totalTimeText.setTextColor(textColor) + binding.totalSinglesText.setTextColor(textColor) + binding.totalDoublesText.setTextColor(textColor) + binding.totalTriplesText.setTextColor(textColor) + binding.totalTetrisesText.setTextColor(textColor) + binding.maxLevelText.setTextColor(textColor) + binding.maxScoreText.setTextColor(textColor) + binding.maxLinesText.setTextColor(textColor) + + // Apply theme to buttons + binding.backButton.setTextColor(textColor) + binding.resetStatsButton.setTextColor(textColor) + } + private fun showResetConfirmationDialog() { AlertDialog.Builder(this) .setTitle("Reset Stats") diff --git a/app/src/main/java/com/mintris/model/HighScoreAdapter.kt b/app/src/main/java/com/mintris/model/HighScoreAdapter.kt index 1f86531..de4ec52 100644 --- a/app/src/main/java/com/mintris/model/HighScoreAdapter.kt +++ b/app/src/main/java/com/mintris/model/HighScoreAdapter.kt @@ -1,5 +1,6 @@ package com.mintris.model +import android.graphics.Color import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,6 +10,8 @@ import com.mintris.R class HighScoreAdapter : RecyclerView.Adapter() { private var highScores: List = emptyList() + private var currentTheme = "theme_classic" // Default theme + private var textColor = Color.WHITE // Default text color fun updateHighScores(newHighScores: List) { highScores = newHighScores @@ -24,14 +27,19 @@ class HighScoreAdapter : RecyclerView.Adapter Color.WHITE + "theme_neon" -> Color.parseColor("#FF00FF") + "theme_monochrome" -> Color.LTGRAY + "theme_retro" -> Color.parseColor("#FF5A5F") + "theme_minimalist" -> Color.BLACK + "theme_galaxy" -> Color.parseColor("#66FCF1") + else -> Color.WHITE + } + + notifyDataSetChanged() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt index d25e9fd..e9260bd 100644 --- a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -286,15 +286,15 @@ class PlayerProgressionManager(context: Context) { private const val KEY_UNLOCKED_BADGES = "unlocked_badges" // XP curve parameters - private const val BASE_XP = 5000.0 // Base XP for level 1 (increased from 2500) - private const val XP_CURVE_FACTOR = 2.2 // Exponential factor for XP curve (increased from 2.0) + private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000) + private const val XP_CURVE_FACTOR = 1.9 // Exponential factor for XP curve (reduced from 2.2) // XP calculation constants - private const val LEVEL_MULTIPLIER = 0.1 // 10% bonus per level - private const val XP_PER_LINE = 10L - private const val TETRIS_XP_BONUS = 50L - private const val PERFECT_CLEAR_XP_BONUS = 200L - private const val TIME_XP_PER_MINUTE = 5L + private const val LEVEL_MULTIPLIER = 0.15 // 15% bonus per level (increased from 10%) + private const val XP_PER_LINE = 15L // Increased from 10 + private const val TETRIS_XP_BONUS = 75L // Increased from 50 + private const val PERFECT_CLEAR_XP_BONUS = 250L // Increased from 200 + private const val TIME_XP_PER_MINUTE = 8L // Increased from 5 // Theme IDs with required levels const val THEME_CLASSIC = "theme_classic" diff --git a/app/src/main/java/com/mintris/ui/ThemeSelector.kt b/app/src/main/java/com/mintris/ui/ThemeSelector.kt index 3be59c7..1653ab2 100644 --- a/app/src/main/java/com/mintris/ui/ThemeSelector.kt +++ b/app/src/main/java/com/mintris/ui/ThemeSelector.kt @@ -86,16 +86,18 @@ class ThemeSelector @JvmOverloads constructor( // Set card background color based on theme setCardBackgroundColor(themeInfo.primaryColor) - // Add stroke for selected theme + // Add more noticeable visual indicator for selected theme if (isSelected) { setContentPadding(4, 4, 4, 4) // Create a gradient drawable for the border val gradientDrawable = android.graphics.drawable.GradientDrawable().apply { setColor(themeInfo.primaryColor) - setStroke(4, Color.WHITE) + setStroke(6, Color.WHITE) // Thicker border cornerRadius = 12f } background = gradientDrawable + // Add glow effect via elevation + cardElevation = 12f } // Set card dimensions @@ -194,9 +196,29 @@ class ThemeSelector @JvmOverloads constructor( card.setOnClickListener { // Only trigger callback if this isn't already the selected theme if (themeId != selectedTheme) { - // Update visual state - themeCards[selectedTheme]?.cardElevation = 2f - card.cardElevation = 8f + // Update previously selected card + themeCards[selectedTheme]?.let { prevCard -> + prevCard.cardElevation = 2f + // Reset any special styling + prevCard.background = null + prevCard.setCardBackgroundColor(getThemes()[selectedTheme]?.primaryColor ?: Color.BLACK) + } + + // Update visual state of newly selected card + card.cardElevation = 12f + + // Flash animation for selection feedback + val flashColor = Color.WHITE + val originalColor = themeInfo.primaryColor + + // Create animator for flash effect + val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor) + flashAnimator.duration = 300 // 300ms + flashAnimator.addUpdateListener { animator -> + val color = animator.animatedValue as Int + card.setCardBackgroundColor(color) + } + flashAnimator.start() // Update selected theme selectedTheme = themeId diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e978405..371cb8c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -263,6 +263,7 @@ android:layout_marginBottom="32dp"> Date: Fri, 28 Mar 2025 11:57:21 -0400 Subject: [PATCH 02/59] Disable diagonal inputs and prevent accidental hard drops. Block back swipe gesture to prevent accidental app exits. --- app/src/main/AndroidManifest.xml | 7 +- app/src/main/java/com/mintris/MainActivity.kt | 84 +++++++++++++++++++ .../main/java/com/mintris/game/GameView.kt | 21 +++-- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aaa37f3..924b03a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:theme="@style/Theme.Mintris.NoActionBar" + android:immersive="true" + android:resizeableActivity="false" + android:excludeFromRecents="false"> diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index e403d6c..2f39436 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -32,6 +32,8 @@ import java.util.* import android.graphics.Color import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import android.graphics.Rect +import android.view.KeyEvent class MainActivity : AppCompatActivity() { @@ -73,6 +75,9 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + // Disable Android back gesture to prevent accidental app exits + disableAndroidBackGesture() + // Initialize game components gameBoard = GameBoard() gameHaptics = GameHaptics(this) @@ -646,4 +651,83 @@ class MainActivity : AppCompatActivity() { else -> Color.WHITE } } + + /** + * Disables the Android system back gesture to prevent accidental exits + */ + private fun disableAndroidBackGesture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Set the entire window to be excluded from the system gesture areas + window.decorView.post { + // Create a list of rectangles representing the edges of the screen to exclude from system gestures + val gestureInsets = window.decorView.rootWindowInsets?.systemGestureInsets + if (gestureInsets != null) { + val leftEdge = Rect(0, 0, 50, window.decorView.height) + val rightEdge = Rect(window.decorView.width - 50, 0, window.decorView.width, window.decorView.height) + val bottomEdge = Rect(0, window.decorView.height - 50, window.decorView.width, window.decorView.height) + + window.decorView.systemGestureExclusionRects = listOf(leftEdge, rightEdge, bottomEdge) + } + } + } + + // Add an on back pressed callback to handle back button/gesture + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + onBackPressedDispatcher.addCallback(this, object : androidx.activity.OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // If we're playing the game, handle it as a pause action instead of exiting + if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) { + gameView.pause() + gameMusic.pause() + showPauseMenu() + binding.pauseStartButton.visibility = View.GONE + binding.resumeButton.visibility = View.VISIBLE + } else if (binding.pauseContainer.visibility == View.VISIBLE) { + // If pause menu is showing, handle as a resume + resumeGame() + } else if (binding.gameOverContainer.visibility == View.VISIBLE) { + // If game over is showing, go back to title + hideGameOver() + showTitleScreen() + } else if (titleScreen.visibility == View.VISIBLE) { + // If title screen is showing, allow normal back behavior (exit app) + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + }) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // For Android 11 (R) to Android 12 (S), use the WindowInsetsController to disable gestures + window.insetsController?.systemBarsBehavior = + android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + /** + * Completely block the hardware back button during gameplay + */ + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + // If back button is pressed + if (keyCode == KeyEvent.KEYCODE_BACK) { + // Handle back button press as a pause action during gameplay + if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) { + gameView.pause() + gameMusic.pause() + showPauseMenu() + binding.pauseStartButton.visibility = View.GONE + binding.resumeButton.visibility = View.VISIBLE + return true // Consume the event + } else if (binding.pauseContainer.visibility == View.VISIBLE) { + // If pause menu is showing, handle as a resume + resumeGame() + return true // Consume the event + } else if (binding.gameOverContainer.visibility == View.VISIBLE) { + // If game over is showing, go back to title + hideGameOver() + showTitleScreen() + return true // Consume the event + } + } + return super.onKeyDown(keyCode, event) + } } \ 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 ce82647..95332db 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -39,7 +39,8 @@ class GameView @JvmOverloads constructor( // Game state private var isRunning = false - private var isPaused = false + var isPaused = false // Changed from private to public to allow access from MainActivity + private var score = 0 // Callbacks var onNextPieceChanged: (() -> Unit)? = null @@ -128,14 +129,16 @@ class GameView @JvmOverloads constructor( private var lastTapTime = 0L private var lastRotationTime = 0L private var lastMoveTime = 0L - private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop + private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels) private val minTapTime = 100L // Minimum time for a tap (in milliseconds) private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) private var lockedDirection: Direction? = null // Track the locked movement direction private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size - private val directionLockThreshold = 1.5f // Threshold for direction lock relative to block size + private val directionLockThreshold = 2.5f // Increased from 1.5f to make direction locking more aggressive + private val isStrictDirectionLock = true // Enable strict direction locking to prevent diagonal inputs + private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture private enum class Direction { HORIZONTAL, VERTICAL @@ -612,12 +615,14 @@ class GameView @JvmOverloads constructor( // Check if movement exceeds threshold if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { - // Determine dominant direction + // Determine dominant direction with stricter criteria if (absDeltaX > absDeltaY * directionLockThreshold) { lockedDirection = Direction.HORIZONTAL } else if (absDeltaY > absDeltaX * directionLockThreshold) { lockedDirection = Direction.VERTICAL } + // If strict direction lock is enabled and we couldn't determine a clear direction, don't set one + // This prevents diagonal movements from being recognized } } @@ -661,8 +666,12 @@ class GameView @JvmOverloads constructor( val deltaY = event.y - startY val deltaX = event.x - startX - // If the movement was fast and downward, treat as hard drop - if (moveTime > 0 && deltaY > blockSize * 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) { + // Only allow hard drops with a deliberate downward swipe + // Requires: predominantly vertical movement, minimum distance, and minimum velocity + if (moveTime > 0 && + deltaY > blockSize * minHardDropDistance && // Require longer swipe for hard drop + (deltaY / moveTime) * 1000 > minSwipeVelocity && + abs(deltaX) < abs(deltaY) * 0.3f) { // Require more purely vertical movement (reduced from 0.5f to 0.3f) gameBoard.hardDrop() invalidate() } else if (moveTime < minTapTime && From 9ab9b534071386da15506105f7bfd4810edbdcb5 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Fri, 28 Mar 2025 12:33:42 -0400 Subject: [PATCH 03/59] 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">