package com.pixelmintdrop 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.view.HapticFeedbackConstants import androidx.appcompat.app.AppCompatActivity import androidx.activity.viewModels // Added import import androidx.lifecycle.Observer // Added import import com.pixelmintdrop.databinding.ActivityMainBinding import com.pixelmintdrop.game.GameHaptics import com.pixelmintdrop.game.GameView import com.pixelmintdrop.game.TitleScreen import com.pixelmintdrop.model.GameBoard import com.pixelmintdrop.audio.GameMusic import com.pixelmintdrop.model.HighScoreManager import com.pixelmintdrop.model.PlayerProgressionManager import com.pixelmintdrop.model.StatsManager import com.pixelmintdrop.ui.ProgressionScreen import com.pixelmintdrop.ui.ThemeSelector import com.pixelmintdrop.ui.BlockSkinSelector import java.text.SimpleDateFormat import java.util.* 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 import android.os.Handler import android.os.Looper import android.view.MotionEvent import com.pixelmintdrop.game.GamepadController import android.view.InputDevice import android.widget.Toast import android.content.BroadcastReceiver import android.content.IntentFilter import android.graphics.drawable.ColorDrawable import androidx.core.content.ContextCompat import android.widget.Button import android.widget.ScrollView import android.widget.ImageButton import android.graphics.drawable.GradientDrawable import android.widget.TextView import android.widget.Switch import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import android.view.ViewGroup import androidx.core.view.updatePadding import kotlin.math.max import kotlin.math.min import kotlin.random.Random class MainActivity : AppCompatActivity(), GamepadController.GamepadConnectionListener, GamepadController.GamepadMenuListener, GamepadController.GamepadNavigationListener { companion object { private const val TAG = "MainActivity" } // ViewModel private val viewModel: MainActivityViewModel by viewModels() // Added ViewModel // UI components private lateinit var binding: ActivityMainBinding private lateinit var gameView: GameView private lateinit var gameHaptics: GameHaptics private lateinit var gameBoard: GameBoard private lateinit var gameMusic: GameMusic private lateinit var titleScreen: TitleScreen private lateinit var highScoreManager: HighScoreManager private lateinit var statsManager: StatsManager private lateinit var progressionManager: PlayerProgressionManager private lateinit var progressionScreen: ProgressionScreen private lateinit var themeSelector: ThemeSelector private lateinit var blockSkinSelector: BlockSkinSelector private var pauseMenuScrollView: ScrollView? = null // Game state private var isSoundEnabled = true private var isMusicEnabled = true private var piecesPlaced = 0 private var gameStartTime = 0L private var selectedLevel = 1 private val maxLevel = 20 private var lastLines = 0 // Track the previous lines count private var lastLinesGroup = 0 // Track which 10-line group we're in (0-9, 10-19, etc.) private var lastRandomLevel = 0 // Track the level at which we last did a random change private var isRandomModeEnabled = false private var currentTheme = PlayerProgressionManager.THEME_CLASSIC private val handler = Handler(Looper.getMainLooper()) // Activity result launcher for high score entry private lateinit var highScoreEntryLauncher: ActivityResultLauncher // Gamepad controller private lateinit var gamepadController: GamepadController // Input device change receiver private val inputDeviceReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { Intent.ACTION_CONFIGURATION_CHANGED -> { // A gamepad might have connected or disconnected checkGamepadConnections() } } } } // Track connected gamepads to detect changes private val connectedGamepads = mutableSetOf() // Track currently selected menu item in pause menu for gamepad navigation private var currentMenuSelection = 0 private val pauseMenuItems = mutableListOf() private val customizationMenuItems = mutableListOf() // Add these new properties at the class level private var currentCustomizationMenuSelection = 0 override fun onCreate(savedInstanceState: Bundle?) { // Register activity result launcher for high score entry highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> // No matter what the result is, we just show the game over container progressionScreen.visibility = View.GONE binding.gameOverContainer.visibility = View.VISIBLE // Keep all game UI elements hidden binding.gameControlsContainer.visibility = View.GONE binding.holdPieceView.visibility = View.GONE binding.nextPieceView.visibility = View.GONE binding.pauseButton.visibility = View.GONE binding.leftControlsPanel?.visibility = View.GONE binding.rightControlsPanel?.visibility = View.GONE } super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Store initial padding values before applying insets val initialPausePadding = Rect(binding.pauseContainer.paddingLeft, binding.pauseContainer.paddingTop, binding.pauseContainer.paddingRight, binding.pauseContainer.paddingBottom) val initialGameOverPadding = Rect(binding.gameOverContainer.paddingLeft, binding.gameOverContainer.paddingTop, binding.gameOverContainer.paddingRight, binding.gameOverContainer.paddingBottom) val initialCustomizationPadding = Rect(binding.customizationContainer.paddingLeft, binding.customizationContainer.paddingTop, binding.customizationContainer.paddingRight, binding.customizationContainer.paddingBottom) val initialProgressionPadding = Rect(binding.progressionScreen.paddingLeft, binding.progressionScreen.paddingTop, binding.progressionScreen.paddingRight, binding.progressionScreen.paddingBottom) // Apply insets to the pause container ViewCompat.setOnApplyWindowInsetsListener(binding.pauseContainer) { view, windowInsets -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) // Apply insets by adding to initial padding view.updatePadding( left = initialPausePadding.left + insets.left, top = initialPausePadding.top + insets.top, right = initialPausePadding.right + insets.right, bottom = initialPausePadding.bottom + insets.bottom ) WindowInsetsCompat.CONSUMED } // Apply insets to the game over container ViewCompat.setOnApplyWindowInsetsListener(binding.gameOverContainer) { view, windowInsets -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) view.updatePadding( left = initialGameOverPadding.left + insets.left, top = initialGameOverPadding.top + insets.top, right = initialGameOverPadding.right + insets.right, bottom = initialGameOverPadding.bottom + insets.bottom ) WindowInsetsCompat.CONSUMED } // Apply insets to the customization container ViewCompat.setOnApplyWindowInsetsListener(binding.customizationContainer) { view, windowInsets -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) view.updatePadding( left = initialCustomizationPadding.left + insets.left, top = initialCustomizationPadding.top + insets.top, right = initialCustomizationPadding.right + insets.right, bottom = initialCustomizationPadding.bottom + insets.bottom ) WindowInsetsCompat.CONSUMED } // Apply insets to the progression screen ViewCompat.setOnApplyWindowInsetsListener(binding.progressionScreen) { view, windowInsets -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) view.updatePadding( left = initialProgressionPadding.left + insets.left, top = initialProgressionPadding.top + insets.top, right = initialProgressionPadding.right + insets.right, bottom = initialProgressionPadding.bottom + insets.bottom ) WindowInsetsCompat.CONSUMED } // Disable Android back gesture to prevent accidental app exits disableAndroidBackGesture() // Initialize game components gameBoard = GameBoard() gameHaptics = GameHaptics(this) gameView = binding.gameView titleScreen = binding.titleScreen gameMusic = GameMusic(this) highScoreManager = HighScoreManager(this) statsManager = StatsManager(this) progressionManager = PlayerProgressionManager(this) themeSelector = binding.customizationThemeSelector!! blockSkinSelector = binding.customizationBlockSkinSelector!! pauseMenuScrollView = binding.pauseMenuScrollView // Observe ViewModel LiveData viewModel.currentScore.observe(this, Observer { newScore -> // Use actual ID from layout - display only the number binding.scoreText.text = newScore.toString() }) viewModel.currentLevel.observe(this, Observer { newLevel -> // Use actual ID from layout - display only the number binding.currentLevelText.text = newLevel.toString() }) // Load random mode setting isRandomModeEnabled = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) .getBoolean("random_mode_enabled", false) // Initialize gamepad controller gamepadController = GamepadController(gameView) gamepadController.setGamepadConnectionListener(this) gamepadController.setGamepadMenuListener(this) gamepadController.setGamepadNavigationListener(this) // Set up touch event forwarding binding.touchInterceptor?.setOnTouchListener { _, event -> if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) { gameView.onTouchEvent(event) true } else { false } } // Set up progression screen progressionScreen = binding.progressionScreen progressionScreen.visibility = View.GONE progressionScreen.onContinue = { progressionScreen.visibility = View.GONE binding.gameOverContainer.visibility = View.VISIBLE // Keep all game UI elements hidden binding.gameControlsContainer.visibility = View.GONE binding.holdPieceView.visibility = View.GONE binding.nextPieceView.visibility = View.GONE binding.pauseButton.visibility = View.GONE binding.leftControlsPanel?.visibility = View.GONE binding.rightControlsPanel?.visibility = View.GONE // Get all themes with "Theme" in their name val themeRewards = progressionManager.getUnlockedThemes().filter { it.contains("Theme", ignoreCase = true) } // Update theme selector if new themes were unlocked if (themeRewards.isNotEmpty()) { updateThemeSelector() } } // Load and apply theme preference currentTheme = progressionManager.getSelectedTheme() applyTheme(currentTheme) // Load and apply block skin preference gameView.setBlockSkin(progressionManager.getSelectedBlockSkin()) // Update block skin selector with current selection blockSkinSelector.updateBlockSkins( progressionManager.getUnlockedBlocks(), gameView.getCurrentBlockSkin(), progressionManager.getPlayerLevel() ) // Set up game view gameView.setGameBoard(gameBoard) gameView.setHaptics(gameHaptics) gameBoard.setStatsManager(statsManager) // Set up theme selector themeSelector.onThemeSelected = { themeId: String -> // 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 titleScreen.onStartGame = { titleScreen.visibility = View.GONE gameView.visibility = View.VISIBLE binding.gameControlsContainer.visibility = View.VISIBLE startGame() } // Initially hide the game view and show title screen gameView.visibility = View.GONE binding.gameControlsContainer.visibility = View.GONE titleScreen.visibility = View.VISIBLE // Set up pause button to show settings menu binding.pauseButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) gameView.pause() gameMusic.pause() showPauseMenu() binding.pauseStartButton.visibility = View.GONE binding.resumeButton.visibility = View.VISIBLE } // Set up next piece preview binding.nextPieceView.setGameView(gameView) gameBoard.onNextPieceChanged = { binding.nextPieceView.invalidate() } // Set up hold piece preview binding.holdPieceView.setGameView(gameView) gameBoard.onPieceLock = { binding.holdPieceView.invalidate() } gameBoard.onPieceMove = { binding.holdPieceView.invalidate() } gameBoard.onPiecePlaced = { piecesPlaced++ } // Set up music toggle binding.musicToggle.setOnClickListener { isMusicEnabled = !isMusicEnabled gameMusic.setEnabled(isMusicEnabled) updateMusicToggleUI() } // Set up callbacks gameView.onGameStateChanged = { score, level, lines -> updateGameStateUI(score, level, lines) } gameView.onGameOver = { finalScore -> // Start animation & pause music gameView.startGameOverAnimation() gameMusic.pause() // Calculate final stats, XP, and high score val timePlayedMs = System.currentTimeMillis() - gameStartTime statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, viewModel.currentLevel.value ?: 1) val xpGained = progressionManager.calculateGameXP( score = finalScore, linesCleared = gameBoard.lines, level = viewModel.currentLevel.value ?: 1, gameTime = timePlayedMs, tSpins = statsManager.getSessionTSpins(), combos = statsManager.getSessionMaxCombo(), quadCount = statsManager.getSessionQuads(), perfectClearCount = statsManager.getSessionPerfectClears() ) val newRewards = progressionManager.addXP(xpGained) val newHighScore = highScoreManager.isHighScore(finalScore) statsManager.endSession() // End session after calculations Log.d(TAG, "Game Over. Score: $finalScore, Level: ${viewModel.currentLevel.value}, Lines: ${gameBoard.lines}, Start Level: $selectedLevel, New High Score: $newHighScore, XP Gained: $xpGained") // Show appropriate screen: Progression or Game Over directly if (xpGained > 0 || newHighScore) { // Delay showing progression slightly to let animation play Handler(Looper.getMainLooper()).postDelayed({ showProgressionScreen(xpGained, newRewards, newHighScore, finalScore) }, 1500) // Delay can be adjusted } else { // No XP, no high score -> show game over screen directly after animation Handler(Looper.getMainLooper()).postDelayed({ showGameOverScreenDirectly(finalScore) }, 1500) // Delay to match progression path } } gameView.onLineClear = { lineCount -> Log.d(TAG, "Received line clear callback: $lineCount lines") // Use enhanced haptic feedback for line clears if (isSoundEnabled) { Log.d(TAG, "Sound is enabled, triggering haptic feedback") try { gameHaptics.vibrateForLineClear(lineCount) Log.d(TAG, "Haptic feedback triggered successfully") } catch (e: Exception) { Log.e(TAG, "Error triggering haptic feedback", e) } } else { Log.d(TAG, "Sound is disabled, skipping haptic feedback") } // Record line clear in stats statsManager.recordLineClear(lineCount) } // Add callbacks for piece movement and locking gameView.onPieceMove = { if (isSoundEnabled) { gameHaptics.vibrateForPieceMove() } } // Set up button click listeners with haptic feedback binding.playAgainButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hideGameOver() gameView.reset() startGame() } binding.resumeButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hidePauseMenu() resumeGame() } binding.settingsButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) toggleSound() } // Set up pause menu buttons binding.pauseStartButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hidePauseMenu() gameView.reset() startGame() } binding.pauseRestartButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hidePauseMenu() gameView.reset() startGame() } binding.highScoresButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) showHighScores() } binding.pauseLevelUpButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) if (selectedLevel < maxLevel) { selectedLevel++ updateLevelSelector() } } binding.pauseLevelDownButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) if (selectedLevel > 1) { selectedLevel-- updateLevelSelector() } } // Set up stats button binding.statsButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) val intent = Intent(this, StatsActivity::class.java) startActivity(intent) } // Set up customization button binding.customizationButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) showCustomizationMenu() } // Set up customization back button binding.customizationBackButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hideCustomizationMenu() } // Initialize level selector updateLevelSelector() // Enable edge-to-edge display if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Use the WindowInsetsController for Android 11+ edge-to-edge display window.insetsController?.let { controller -> controller.hide(android.view.WindowInsets.Type.statusBars() or android.view.WindowInsets.Type.navigationBars()) controller.systemBarsBehavior = android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // For Android 10, use the deprecated method @Suppress("DEPRECATION") window.setDecorFitsSystemWindows(false) } // Initialize pause menu items for gamepad navigation initPauseMenuNavigation() } /** * Update UI with current game state */ private fun updateUI(score: Int, level: Int, lines: Int) { binding.scoreText.text = score.toString() binding.currentLevelText.text = level.toString() binding.linesText.text = lines.toString() binding.comboText.text = gameBoard.getCombo().toString() // Force redraw of next piece preview binding.nextPieceView.invalidate() } /** * Shows the final game over screen with stats. */ private fun showGameOverScreenDirectly(score: Int) { Log.d(TAG, "Showing final game over screen with score: $score") // Ensure game UI is hidden binding.gameControlsContainer.visibility = View.GONE binding.holdPieceView.visibility = View.GONE binding.nextPieceView.visibility = View.GONE binding.pauseButton.visibility = View.GONE binding.leftControlsPanel?.visibility = View.GONE binding.rightControlsPanel?.visibility = View.GONE progressionScreen.visibility = View.GONE // Ensure progression is hidden // Update session stats display in the gameOverContainer val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) timeFormat.timeZone = TimeZone.getTimeZone("UTC") val gameTime = System.currentTimeMillis() - gameStartTime // Recalculate or pass from onGameOver? // Let's recalculate for simplicity here. // Set text directly using interpolation (workaround attempt) binding.sessionScoreText.text = "Score: $score" binding.sessionLinesText.text = "Lines: ${gameBoard.lines}" binding.sessionPiecesText.text = "Pieces: $piecesPlaced" binding.sessionTimeText.text = "Time: ${timeFormat.format(gameTime)}" binding.sessionLevelText.text = "Level: ${viewModel.currentLevel.value ?: 1}" binding.sessionSinglesText.text = "Singles: ${statsManager.getSessionSingles()}" binding.sessionDoublesText.text = "Doubles: ${statsManager.getSessionDoubles()}" binding.sessionTriplesText.text = "Triples: ${statsManager.getSessionTriples()}" binding.sessionQuadsText.text = "Quads: ${statsManager.getSessionQuads()}" // Make the container visible binding.gameOverContainer.visibility = View.VISIBLE // Play game over sound if not already played by progression if (isSoundEnabled && progressionScreen.visibility != View.VISIBLE) { gameMusic.playGameOver() } // Vibrate if not already vibrated by progression if (progressionScreen.visibility != View.VISIBLE) { vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK) } } /** * Show high score entry screen */ private fun showHighScoreEntry(score: Int) { val intent = Intent(this, HighScoreEntryActivity::class.java).apply { putExtra("score", score) putExtra("level", viewModel.currentLevel.value ?: 1) // Read from ViewModel } // Use the launcher instead of startActivity highScoreEntryLauncher.launch(intent) } /** * Hide game over screen */ private fun hideGameOver() { binding.gameOverContainer.visibility = View.GONE progressionScreen.visibility = View.GONE } /** * Show customization menu */ private fun showCustomizationMenu() { binding.pauseContainer.visibility = View.GONE binding.customizationContainer.visibility = View.VISIBLE // Get theme colors val themeColor = getThemeColor(currentTheme) val backgroundColor = getThemeBackgroundColor(currentTheme) // Apply background color to customization container binding.customizationContainer.setBackgroundColor(backgroundColor) // Update level badge with player's level and theme color binding.customizationLevelBadge.setLevel(progressionManager.getPlayerLevel()) binding.customizationLevelBadge.setThemeColor(themeColor) // Apply theme color to all text views in the container applyThemeColorToTextViews(binding.customizationContainer, themeColor) // Update theme selector with available themes binding.customizationThemeSelector?.updateThemes( unlockedThemes = progressionManager.getUnlockedThemes(), currentTheme = currentTheme ) // Update block skin selector with available skins binding.customizationBlockSkinSelector?.updateBlockSkins( progressionManager.getUnlockedBlocks(), gameView.getCurrentBlockSkin(), progressionManager.getPlayerLevel() ) // Set up block skin selection callback binding.customizationBlockSkinSelector?.onBlockSkinSelected = { blockSkin -> gameView.setBlockSkin(blockSkin) progressionManager.setSelectedBlockSkin(blockSkin) gameHaptics.vibrateForPieceLock() } // Initialize customization menu items for gamepad navigation initCustomizationMenuNavigation() // Set up random mode switch binding.randomModeSwitch?.apply { isChecked = isRandomModeEnabled isEnabled = progressionManager.getPlayerLevel() >= 5 setOnCheckedChangeListener { _, isChecked -> isRandomModeEnabled = isChecked getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) .edit() .putBoolean("random_mode_enabled", isChecked) .apply() } } } private fun initCustomizationMenuNavigation() { customizationMenuItems.clear() // Add items in order customizationMenuItems.addAll(listOf( binding.customizationThemeSelector, binding.customizationBlockSkinSelector, binding.randomModeSwitch, binding.customizationBackButton ).filterNotNull().filter { it.visibility == View.VISIBLE }) // Set up focus change listener for scrolling binding.customizationMenuScrollView.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { // When scroll view gets focus, scroll to focused item val focusedView = currentFocus if (focusedView != null) { val scrollView = binding.customizationMenuScrollView val itemTop = focusedView.top val scrollViewHeight = scrollView.height // Calculate scroll position to center the focused item val scrollY = itemTop - (scrollViewHeight / 2) + (focusedView.height / 2) scrollView.smoothScrollTo(0, scrollY) } } } // Set up focus handling between items customizationMenuItems.forEachIndexed { index, item -> item.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { currentCustomizationMenuSelection = index highlightCustomizationMenuItem(index) } } } } /** * Hide customization menu */ private fun hideCustomizationMenu() { binding.customizationContainer.visibility = View.GONE binding.pauseContainer.visibility = View.VISIBLE } /** * Show settings menu */ private fun showPauseMenu() { binding.pauseContainer.visibility = View.VISIBLE binding.customizationContainer.visibility = View.GONE // Set button visibility based on game state if (gameView.isPaused) { binding.resumeButton.visibility = View.VISIBLE binding.pauseStartButton.visibility = View.GONE } else { // This case might happen if pause is triggered before game start (e.g., from title) binding.resumeButton.visibility = View.GONE binding.pauseStartButton.visibility = View.VISIBLE } // Check if we're in landscape mode (used for finding specific views, not for order) val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE // Update level badge binding.pauseLevelBadge?.setLevel(progressionManager.getPlayerLevel()) binding.pauseLevelBadge?.setThemeColor(getThemeColor(currentTheme)) // Get theme colors val themeColor = getThemeColor(currentTheme) val backgroundColor = getThemeBackgroundColor(currentTheme) // Apply background color to pause container binding.pauseContainer.setBackgroundColor(backgroundColor) // Apply theme colors to buttons and toggles binding.pauseStartButton.setTextColor(themeColor) binding.pauseRestartButton.setTextColor(themeColor) binding.resumeButton.setTextColor(themeColor) binding.highScoresButton.setTextColor(themeColor) binding.statsButton.setTextColor(themeColor) binding.pauseLevelUpButton?.setTextColor(themeColor) // Safe call binding.pauseLevelDownButton?.setTextColor(themeColor) // Safe call binding.settingsButton?.setTextColor(themeColor) // Safe call for sound toggle button text binding.musicToggle?.setColorFilter(themeColor) // Safe call binding.customizationButton?.setTextColor(themeColor) // Safe call // Apply theme colors to text elements (using safe calls) binding.settingsTitle?.setTextColor(themeColor) binding.selectLevelText?.setTextColor(themeColor) binding.musicText?.setTextColor(themeColor) binding.pauseLevelText?.setTextColor(themeColor) // Apply to portrait container as well if needed (assuming root or specific container) applyThemeColorToTextViews(binding.pauseContainer, themeColor) // Apply to main container // Reset scroll position binding.pauseMenuScrollView?.scrollTo(0, 0) findLandscapeScrollView()?.scrollTo(0, 0) // Initialize pause menu navigation (builds the list of navigable items) initPauseMenuNavigation() // Reset selection index (will be set and highlighted in showPauseMenu) currentMenuSelection = 0 // Highlight the initially selected menu item if (pauseMenuItems.isNotEmpty()) { highlightMenuItem(currentMenuSelection) } } /** Helper to apply theme color to TextViews within a container, avoiding selectors */ private fun applyThemeColorToTextViews(container: View?, themeColor: Int) { if (container == null) return if (container is android.view.ViewGroup) { for (i in 0 until container.childCount) { val child = container.getChildAt(i) if (child is TextView) { // Check if parent is a selector var parent = child.parent var isInSelector = false while (parent != null) { if (parent is ThemeSelector || parent is BlockSkinSelector) { isInSelector = true break } if (parent === container) break // Stop at the container boundary parent = parent.parent } if (!isInSelector) { child.setTextColor(themeColor) } } else if (child is android.view.ViewGroup) { // Recurse only if not a selector if (child !is ThemeSelector && child !is BlockSkinSelector) { applyThemeColorToTextViews(child, themeColor) } } } } } /** Helper to get background color based on theme */ private fun getThemeBackgroundColor(themeId: String): Int { return 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 } } /** * Hide settings menu */ private fun hidePauseMenu() { binding.pauseContainer.visibility = View.GONE } /** * Toggle sound on/off */ private fun toggleSound() { isSoundEnabled = !isSoundEnabled binding.settingsButton.text = getString( if (isSoundEnabled) R.string.sound_on else R.string.sound_off ) // Vibrate to provide feedback vibrate(VibrationEffect.EFFECT_CLICK) } /** * Update the level selector display */ private fun updateLevelSelector() { binding.pauseLevelText.text = selectedLevel.toString() gameBoard.updateLevel(selectedLevel) } /** * Trigger device vibration with predefined effect */ private fun vibrate(effectId: Int) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // For Android 12+ (API 31+) val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager val vibrator = vibratorManager.defaultVibrator vibrator.vibrate(VibrationEffect.createPredefined(effectId)) } else { // For older Android versions @Suppress("DEPRECATION") val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator vibrator.vibrate(VibrationEffect.createPredefined(effectId)) } } private fun updateMusicToggleUI() { binding.musicToggle.setImageResource( if (isMusicEnabled) R.drawable.ic_volume_up else R.drawable.ic_volume_off ) } /** * Start a new game */ private fun startGame() { Log.d(TAG, "Starting game at level $selectedLevel") // Reset game state viewModel.resetGame() // Resets score and level in ViewModel viewModel.setLevel(selectedLevel) // Set initial level from selection piecesPlaced = 0 gameStartTime = System.currentTimeMillis() lastLines = 0 lastLinesGroup = 0 lastRandomLevel = 0 // Observers will update scoreText and currentLevelText // Reset game view and game board gameBoard.reset() gameView.reset() // Ensure block skin is properly set val selectedSkin = progressionManager.getSelectedBlockSkin() gameView.setBlockSkin(selectedSkin) // Update selectors to refresh UI state blockSkinSelector.updateBlockSkins( progressionManager.getUnlockedBlocks(), selectedSkin, progressionManager.getPlayerLevel() ) // Ensure game UI elements are visible and others hidden binding.gameView.visibility = View.VISIBLE binding.gameControlsContainer.visibility = View.VISIBLE binding.holdPieceView.visibility = View.VISIBLE binding.nextPieceView.visibility = View.VISIBLE binding.pauseButton.visibility = View.VISIBLE binding.gameOverContainer.visibility = View.GONE binding.pauseContainer.visibility = View.GONE titleScreen.visibility = View.GONE progressionScreen.visibility = View.GONE // Show landscape specific controls if needed if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { binding.leftControlsPanel?.visibility = View.VISIBLE binding.rightControlsPanel?.visibility = View.VISIBLE } // Configure callbacks gameView.onGameStateChanged = { score, level, lines -> // We'll adapt updateGameStateUI later to use ViewModel updateGameStateUI(score, level, lines) } gameView.onGameOver = { finalScore -> // Start animation & pause music gameView.startGameOverAnimation() gameMusic.pause() // Calculate final stats, XP, and high score val timePlayedMs = System.currentTimeMillis() - gameStartTime statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, viewModel.currentLevel.value ?: 1) val xpGained = progressionManager.calculateGameXP( score = finalScore, linesCleared = gameBoard.lines, level = viewModel.currentLevel.value ?: 1, gameTime = timePlayedMs, tSpins = statsManager.getSessionTSpins(), combos = statsManager.getSessionMaxCombo(), quadCount = statsManager.getSessionQuads(), perfectClearCount = statsManager.getSessionPerfectClears() ) val newRewards = progressionManager.addXP(xpGained) val newHighScore = highScoreManager.isHighScore(finalScore) statsManager.endSession() // End session after calculations Log.d(TAG, "Game Over. Score: $finalScore, Level: ${viewModel.currentLevel.value}, Lines: ${gameBoard.lines}, Start Level: $selectedLevel, New High Score: $newHighScore, XP Gained: $xpGained") // Show appropriate screen: Progression or Game Over directly if (xpGained > 0 || newHighScore) { // Delay showing progression slightly to let animation play Handler(Looper.getMainLooper()).postDelayed({ showProgressionScreen(xpGained, newRewards, newHighScore, finalScore) }, 1500) // Delay can be adjusted } else { // No XP, no high score -> show game over screen directly after animation Handler(Looper.getMainLooper()).postDelayed({ showGameOverScreenDirectly(finalScore) }, 1500) // Delay to match progression path } } // Connect line clear callback to gamepad rumble gameView.onLineClear = { lineCount -> // Vibrate phone gameHaptics.vibrateForLineClear(lineCount) // Vibrate gamepad if connected gamepadController.vibrateForLineClear(lineCount) // Record line clear in stats statsManager.recordLineClear(lineCount) } // Reset pause button state binding.pauseStartButton.visibility = View.VISIBLE binding.resumeButton.visibility = View.GONE // Start background music if enabled if (isMusicEnabled) { gameMusic.start() } // Start the game gameView.start() gameMusic.setEnabled(isMusicEnabled) // Reset session stats statsManager.startNewSession() progressionManager.startNewSession() gameBoard.updateLevel(selectedLevel) } private fun restartGame() { gameBoard.reset() gameView.visibility = View.VISIBLE gameView.start() showPauseMenu() } private fun resumeGame() { gameView.resume() if (isMusicEnabled) { gameMusic.resume() } // Force a redraw to ensure pieces aren't frozen gameView.invalidate() } override fun onPause() { super.onPause() if (gameView.visibility == View.VISIBLE) { gameView.pause() gameMusic.pause() } // Unregister broadcast receiver try { unregisterReceiver(inputDeviceReceiver) } catch (e: IllegalArgumentException) { // Receiver wasn't registered } } override fun onResume() { super.onResume() // Register for input device changes val filter = IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED) registerReceiver(inputDeviceReceiver, filter) // Check for already connected gamepads checkGamepadConnections() // If we're on the title screen, don't auto-resume the game if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) { resumeGame() } // Update theme selector with available themes when pause screen appears updateThemeSelector() } override fun onDestroy() { super.onDestroy() gameMusic.release() } /** * Show title screen (for game restart) */ private fun showTitleScreen() { gameView.reset() gameView.visibility = View.GONE binding.gameControlsContainer.visibility = View.GONE binding.gameOverContainer.visibility = View.GONE binding.pauseContainer.visibility = View.GONE titleScreen.visibility = View.VISIBLE titleScreen.applyTheme(currentTheme) } /** * Show high scores */ private fun showHighScores() { val intent = Intent(this, HighScoresActivity::class.java) startActivity(intent) } /** * Update the theme selector with unlocked themes */ private fun updateThemeSelector() { binding.customizationThemeSelector?.updateThemes( unlockedThemes = progressionManager.getUnlockedThemes(), currentTheme = currentTheme ) } /** * Apply a theme to the game */ private fun applyTheme(themeId: String) { currentTheme = themeId val themeColor = 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 } // Get background color for the theme 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 } // Apply background color to root view binding.root.setBackgroundColor(backgroundColor) // Apply theme color to title screen titleScreen.setThemeColor(themeColor) titleScreen.setBackgroundColor(backgroundColor) // Apply theme color to game over screen binding.gameOverContainer.setBackgroundColor(backgroundColor) binding.gameOverText.setTextColor(themeColor) binding.scoreText.setTextColor(themeColor) binding.currentLevelText.setTextColor(themeColor) binding.linesText.setTextColor(themeColor) binding.comboText.setTextColor(themeColor) binding.playAgainButton.setTextColor(themeColor) binding.playAgainButton.setBackgroundResource(android.R.color.transparent) binding.playAgainButton.setTextSize(24f) // Apply theme to progression screen (it will handle its own colors) progressionScreen.applyTheme(themeId) // Apply theme color to game view gameView.setThemeColor(themeColor) gameView.setBackgroundColor(backgroundColor) // Save theme preference progressionManager.setSelectedTheme(themeId) } /** * Get the appropriate color for the current theme */ private fun getThemeColor(themeId: String): Int { return 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 } } /** * 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 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Use the new API for Android 11+ window.decorView.rootWindowInsets?.let { insets -> 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) } } else { // Use the deprecated API for Android 10 @Suppress("DEPRECATION") 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 } } /** * GamepadConnectionListener implementation */ override fun onGamepadConnected(gamepadName: String) { runOnUiThread { Toast.makeText(this, "Gamepad connected: $gamepadName", Toast.LENGTH_SHORT).show() // Set gamepad connected state in haptics gameHaptics.setGamepadConnected(true) // Provide haptic feedback for gamepad connection gameHaptics.vibrateForPieceLock() // Show gamepad help if not shown before if (!hasSeenGamepadHelp()) { showGamepadHelpDialog() } } } override fun onGamepadDisconnected(gamepadName: String) { runOnUiThread { Toast.makeText(this, "Gamepad disconnected: $gamepadName", Toast.LENGTH_SHORT).show() // Set gamepad disconnected state in haptics gameHaptics.setGamepadConnected(false) } } /** * Show gamepad help dialog */ private fun showGamepadHelpDialog() { val dialogView = layoutInflater.inflate(R.layout.gamepad_help_dialog, null) val dialog = android.app.AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Dialog_NoActionBar) .setView(dialogView) .setCancelable(true) .create() // Set up dismiss button dialogView.findViewById(R.id.gamepad_help_dismiss_button).setOnClickListener { dialog.dismiss() markGamepadHelpSeen() } dialog.show() } /** * Check if user has seen the gamepad help */ private fun hasSeenGamepadHelp(): Boolean { val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) return prefs.getBoolean("has_seen_gamepad_help", false) } /** * Mark that user has seen the gamepad help */ private fun markGamepadHelpSeen() { val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) prefs.edit().putBoolean("has_seen_gamepad_help", true).apply() } /** * Check for connected/disconnected gamepads */ private fun checkGamepadConnections() { val currentGamepads = GamepadController.getConnectedGamepadsInfo().toSet() // Find newly connected gamepads currentGamepads.filter { it !in connectedGamepads }.forEach { gamepadName -> onGamepadConnected(gamepadName) } // Find disconnected gamepads connectedGamepads.filter { it !in currentGamepads }.forEach { gamepadName -> onGamepadDisconnected(gamepadName) } // Update the stored list of connected gamepads connectedGamepads.clear() connectedGamepads.addAll(currentGamepads) } /** * Handle key events for both device controls and gamepads */ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { if (event == null) return super.onKeyDown(keyCode, event) // First check if it's a gamepad input if (GamepadController.isGamepad(event.device)) { // Handle title screen start with gamepad if (titleScreen.visibility == View.VISIBLE && (keyCode == KeyEvent.KEYCODE_BUTTON_A || keyCode == KeyEvent.KEYCODE_BUTTON_START)) { titleScreen.performClick() return true } // If gamepad input was handled by the controller, consume the event if (gamepadController.handleKeyEvent(keyCode, event)) { return true } } // If not handled as gamepad, handle as regular key event // Handle back button if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) { // 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.customizationContainer.visibility == View.VISIBLE) { // If customization menu is showing, hide it hideCustomizationMenu() 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) } /** * Handle motion events for gamepad analog sticks */ override fun onGenericMotionEvent(event: MotionEvent): Boolean { // Check if it's a gamepad motion event (analog sticks) if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) { if (gamepadController.handleMotionEvent(event)) { return true } } return super.onGenericMotionEvent(event) } private fun showProgressionScreen(xpGained: Long, newRewards: List, isNewHighScore: Boolean, finalScore: Int) { // Apply theme before showing the screen progressionScreen.applyTheme(currentTheme) // Hide game/other UI elements binding.gameControlsContainer.visibility = View.GONE binding.holdPieceView.visibility = View.GONE binding.nextPieceView.visibility = View.GONE binding.pauseButton.visibility = View.GONE // Hide landscape panels if they exist binding.leftControlsPanel?.visibility = View.GONE binding.rightControlsPanel?.visibility = View.GONE // Show the progression screen binding.gameOverContainer.visibility = View.GONE progressionScreen.visibility = View.VISIBLE // Display progression data progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme) // Set up the continue action progressionScreen.onContinue = { if (isNewHighScore) { showHighScoreEntry(finalScore) } else { // No high score, just show the final game over screen showGameOverScreenDirectly(finalScore) } } } /** * Implements GamepadMenuListener to handle start button press */ override fun onPauseRequested() { runOnUiThread { if (binding.customizationContainer.visibility == View.VISIBLE) { // If customization menu is showing, hide it hideCustomizationMenu() } else 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 hidePauseMenu() resumeGame() } else if (titleScreen.visibility == View.VISIBLE) { // If title screen is showing, start the game titleScreen.performClick() } } } /** * Implements GamepadNavigationListener to handle menu navigation */ override fun onMenuUp() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE) { moveMenuSelectionUp() scrollToSelectedItem() } else if (binding.customizationContainer.visibility == View.VISIBLE) { moveCustomizationMenuSelectionUp() scrollToSelectedCustomizationItem() } } } override fun onMenuDown() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE) { moveMenuSelectionDown() scrollToSelectedItem() } else if (binding.customizationContainer.visibility == View.VISIBLE) { moveCustomizationMenuSelectionDown() scrollToSelectedCustomizationItem() } } } override fun onMenuLeft() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) { val selectedItem = pauseMenuItems.getOrNull(currentMenuSelection) when (selectedItem) { is ThemeSelector -> selectedItem.focusPreviousItem() is BlockSkinSelector -> selectedItem.focusPreviousItem() } } else if (binding.customizationContainer.visibility == View.VISIBLE && customizationMenuItems.isNotEmpty()) { val selectedItem = customizationMenuItems.getOrNull(currentCustomizationMenuSelection) when (selectedItem) { is ThemeSelector -> selectedItem.focusPreviousItem() is BlockSkinSelector -> selectedItem.focusPreviousItem() } } } } override fun onMenuRight() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) { val selectedItem = pauseMenuItems.getOrNull(currentMenuSelection) when (selectedItem) { is ThemeSelector -> selectedItem.focusNextItem() is BlockSkinSelector -> selectedItem.focusNextItem() } } else if (binding.customizationContainer.visibility == View.VISIBLE && customizationMenuItems.isNotEmpty()) { val selectedItem = customizationMenuItems.getOrNull(currentCustomizationMenuSelection) when (selectedItem) { is ThemeSelector -> selectedItem.focusNextItem() is BlockSkinSelector -> selectedItem.focusNextItem() } } } } override fun onMenuSelect() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) { activateSelectedMenuItem() } else if (binding.customizationContainer.visibility == View.VISIBLE && customizationMenuItems.isNotEmpty()) { activateSelectedCustomizationMenuItem() } else if (titleScreen.visibility == View.VISIBLE) { titleScreen.performClick() } else if (progressionScreen.visibility == View.VISIBLE) { progressionScreen.performContinue() } else if (binding.gameOverContainer.visibility == View.VISIBLE) { binding.playAgainButton.performClick() } } } /** * Initialize pause menu items for gamepad navigation */ private fun initPauseMenuNavigation() { pauseMenuItems.clear() // Check landscape mode only to select the *correct instance* of the view val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE // Define the unified order (Portrait Order) val orderedViews = mutableListOf() // Group 1: Game control buttons (Resume/Start New Game, Restart) if (binding.resumeButton.visibility == View.VISIBLE) { orderedViews.add(binding.resumeButton) } if (binding.pauseStartButton.visibility == View.VISIBLE) { orderedViews.add(binding.pauseStartButton) } orderedViews.add(binding.pauseRestartButton) // Group 2: Stats and Scoring orderedViews.add(binding.highScoresButton) orderedViews.add(binding.statsButton) // Group 3: Level selection (use safe calls) orderedViews.add(binding.pauseLevelUpButton) orderedViews.add(binding.pauseLevelDownButton) // Group 4: Sound and Music controls (Moved UP) - Use safe calls orderedViews.add(binding.settingsButton) // Sound toggle (Button) orderedViews.add(binding.musicToggle) // Music toggle (ImageButton) // Group 5: Customization Menu orderedViews.add(binding.customizationButton) // Add the customization button // Add all non-null, visible items from the defined order to the final navigation list pauseMenuItems.addAll(orderedViews.filterNotNull().filter { it.visibility == View.VISIBLE }) } /** * Highlight the currently selected menu item */ private fun highlightMenuItem(index: Int) { // Ensure index is valid if (pauseMenuItems.isEmpty() || index < 0 || index >= pauseMenuItems.size) { Log.w(TAG, "highlightMenuItem called with invalid index: $index or empty items") return } val themeColor = getThemeColor(currentTheme) val highlightBorderColor = themeColor // Use theme color for the main highlight border pauseMenuItems.forEachIndexed { i, item -> val isSelected = (i == index) // Reset common properties item.alpha = if (isSelected) 1.0f else 0.7f if (item is Button || item is ImageButton) { item.scaleX = 1.0f item.scaleY = 1.0f } // Reset when (item) { is Button -> item.background = ColorDrawable(Color.TRANSPARENT) is ImageButton -> item.background = null // Let default background handle non-selected is ThemeSelector -> { item.background = ColorDrawable(Color.TRANSPARENT) item.setHasFocus(false) // Explicitly remove component focus } is BlockSkinSelector -> { item.background = ColorDrawable(Color.TRANSPARENT) item.setHasFocus(false) // Explicitly remove component focus } } // Apply highlight to the selected item if (isSelected) { // Apply scaling to buttons if (item is Button || item is ImageButton) { item.scaleX = 1.1f item.scaleY = 1.1f } // Add border/background based on type val borderWidth = 4 // Unified border width for highlight val cornerRadius = 12f when (item) { is Button -> { // Attempt to use a themed drawable, fallback to simple border ContextCompat.getDrawable(this, R.drawable.menu_item_selected)?.mutate()?.let { if (it is GradientDrawable) { it.setStroke(borderWidth, highlightBorderColor) item.background = it } else { // Fallback for non-gradient drawable (e.g., tinting) it.setTint(highlightBorderColor) item.background = it } } ?: run { // Fallback to simple border if drawable not found var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable } } is ImageButton -> { // Similar handling for ImageButton ContextCompat.getDrawable(this, R.drawable.menu_item_selected)?.mutate()?.let { if (it is GradientDrawable) { it.setStroke(borderWidth, highlightBorderColor) item.background = it } else { it.setTint(highlightBorderColor) item.background = it } } ?: run { var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable } } } } } // Provide haptic feedback for selection change gameHaptics.vibrateForPieceMove() // Ensure the selected item is visible scrollToSelectedItem() } /** * Move menu selection up */ private fun moveMenuSelectionUp() { if (pauseMenuItems.isEmpty()) return // Calculate new selection index with wrapping val previousSelection = currentMenuSelection currentMenuSelection = (currentMenuSelection - 1) // Prevent wrapping from bottom to top for more intuitive navigation if (currentMenuSelection < 0) { currentMenuSelection = 0 return // Already at top, don't provide feedback } // Only provide feedback if selection actually changed if (previousSelection != currentMenuSelection) { highlightMenuItem(currentMenuSelection) } } /** * Move menu selection down */ private fun moveMenuSelectionDown() { if (pauseMenuItems.isEmpty()) return // Calculate new selection index with wrapping val previousSelection = currentMenuSelection currentMenuSelection = (currentMenuSelection + 1) // Prevent wrapping from bottom to top for more intuitive navigation if (currentMenuSelection >= pauseMenuItems.size) { currentMenuSelection = pauseMenuItems.size - 1 return // Already at bottom, don't provide feedback } // Only provide feedback if selection actually changed if (previousSelection != currentMenuSelection) { highlightMenuItem(currentMenuSelection) } } /** * Scroll the selected menu item into view */ private fun scrollToSelectedItem() { if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return val selectedItem = pauseMenuItems[currentMenuSelection] // Determine which scroll view to use based on orientation val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE val scrollView = if (isLandscape) { findLandscapeScrollView() } else { binding.pauseMenuScrollView } ?: return // Delay the scrolling slightly to ensure measurements are correct scrollView.post { try { // Calculate item's position in the ScrollView's coordinate system val scrollBounds = Rect() scrollView.getHitRect(scrollBounds) // Get item's location on screen val itemLocation = IntArray(2) selectedItem.getLocationOnScreen(itemLocation) // Get ScrollView's location on screen val scrollLocation = IntArray(2) scrollView.getLocationOnScreen(scrollLocation) // Calculate relative position val itemY = itemLocation[1] - scrollLocation[1] // Get the top and bottom of the item relative to the ScrollView val itemTop = itemY val itemBottom = itemY + selectedItem.height // Get the visible height of the ScrollView val visibleHeight = scrollView.height // Add padding for better visibility val padding = 50 // Calculate the scroll target position val scrollY = scrollView.scrollY // If item is above visible area if (itemTop < padding) { scrollView.smoothScrollTo(0, scrollY + (itemTop - padding)) } // If item is below visible area else if (itemBottom > visibleHeight - padding) { scrollView.smoothScrollTo(0, scrollY + (itemBottom - visibleHeight + padding)) } } catch (e: Exception) { // Handle any exceptions that might occur during scrolling Log.e(TAG, "Error scrolling to selected item", e) } } } /** * Activate the currently selected menu item */ private fun activateSelectedMenuItem() { if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return val selectedItem = pauseMenuItems[currentMenuSelection] // Vibrate regardless of item type for consistent feedback gameHaptics.vibrateForPieceLock() when (selectedItem) { is ThemeSelector -> { // Confirm the internally focused theme selectedItem.confirmSelection() // Applying the theme might change colors, so refresh the pause menu display // Need to delay slightly to allow theme application to potentially finish Handler(Looper.getMainLooper()).postDelayed({ showPauseMenu() }, 100) } is BlockSkinSelector -> { // Confirm the internally focused block skin selectedItem.confirmSelection() // Skin change doesn't affect menu colors, no refresh needed here } else -> { // Handle other menu items with a standard click selectedItem.performClick() } } } /** * Find any ScrollView in the pause container for landscape mode */ private fun findLandscapeScrollView(): ScrollView? { // First try to find a ScrollView directly in the landscape layout val pauseContainer = findViewById(R.id.pauseContainer) ?: return null // Try to find any ScrollView in the pause container if (pauseContainer is android.view.ViewGroup) { // Search for ScrollView in the container fun findScrollView(view: android.view.View): ScrollView? { if (view is ScrollView && view.visibility == View.VISIBLE) { return view } else if (view is android.view.ViewGroup) { for (i in 0 until view.childCount) { val child = view.getChildAt(i) val scrollView = findScrollView(child) if (scrollView != null) { return scrollView } } } return null } // Try to find ScrollView in the container val scrollView = findScrollView(pauseContainer) if (scrollView != null && scrollView != binding.pauseMenuScrollView) { return scrollView } } // If no landscape ScrollView is found, fall back to the default one return binding.pauseMenuScrollView } // Add these new methods for customization menu navigation private fun moveCustomizationMenuSelectionUp() { if (customizationMenuItems.isEmpty()) return val previousSelection = currentCustomizationMenuSelection currentCustomizationMenuSelection = (currentCustomizationMenuSelection - 1) if (currentCustomizationMenuSelection < 0) { currentCustomizationMenuSelection = 0 return } if (previousSelection != currentCustomizationMenuSelection) { highlightCustomizationMenuItem(currentCustomizationMenuSelection) } } private fun moveCustomizationMenuSelectionDown() { if (customizationMenuItems.isEmpty()) return val previousSelection = currentCustomizationMenuSelection currentCustomizationMenuSelection = (currentCustomizationMenuSelection + 1) if (currentCustomizationMenuSelection >= customizationMenuItems.size) { currentCustomizationMenuSelection = customizationMenuItems.size - 1 return } if (previousSelection != currentCustomizationMenuSelection) { highlightCustomizationMenuItem(currentCustomizationMenuSelection) } } private fun highlightCustomizationMenuItem(index: Int) { if (customizationMenuItems.isEmpty() || index < 0 || index >= customizationMenuItems.size) { Log.w(TAG, "highlightCustomizationMenuItem called with invalid index: $index or empty items") return } val themeColor = getThemeColor(currentTheme) val highlightBorderColor = themeColor val borderWidth = 4 // Define border width for highlight val cornerRadius = 12f customizationMenuItems.forEachIndexed { i, item -> val isSelected = (i == index) item.alpha = if (isSelected) 1.0f else 0.7f if (item is Button || item is ImageButton) { item.scaleX = 1.0f item.scaleY = 1.0f } when (item) { is Button -> item.background = ColorDrawable(Color.TRANSPARENT) is ImageButton -> item.background = null is ThemeSelector -> { item.background = ColorDrawable(Color.TRANSPARENT) item.setHasFocus(false) } is BlockSkinSelector -> { item.background = ColorDrawable(Color.TRANSPARENT) item.setHasFocus(false) } is Switch -> { item.background = ColorDrawable(Color.TRANSPARENT) } } if (isSelected) { when (item) { is Button -> { var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable } is ImageButton -> { var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable } is ThemeSelector -> { var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable item.setHasFocus(true) } is BlockSkinSelector -> { var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable item.setHasFocus(true) } is Switch -> { var gradientDrawable = GradientDrawable().apply { setColor(Color.TRANSPARENT) setStroke(borderWidth, highlightBorderColor) setCornerRadius(cornerRadius) } item.background = gradientDrawable } } } } gameHaptics.vibrateForPieceMove() scrollToSelectedCustomizationItem() } private fun scrollToSelectedCustomizationItem() { if (customizationMenuItems.isEmpty() || currentCustomizationMenuSelection < 0 || currentCustomizationMenuSelection >= customizationMenuItems.size) return val selectedItem = customizationMenuItems[currentCustomizationMenuSelection] val scrollView = binding.customizationMenuScrollView scrollView.post { try { val scrollBounds = Rect() scrollView.getHitRect(scrollBounds) val itemLocation = IntArray(2) selectedItem.getLocationOnScreen(itemLocation) val scrollLocation = IntArray(2) scrollView.getLocationOnScreen(scrollLocation) val itemY = itemLocation[1] - scrollLocation[1] val itemTop = itemY val itemBottom = itemY + selectedItem.height val visibleHeight = scrollView.height val padding = 50 val scrollY = scrollView.scrollY if (itemTop < padding) { scrollView.smoothScrollTo(0, scrollY + (itemTop - padding)) } else if (itemBottom > visibleHeight - padding) { scrollView.smoothScrollTo(0, scrollY + (itemBottom - visibleHeight + padding)) } } catch (e: Exception) { Log.e(TAG, "Error scrolling to selected customization item", e) } } } private fun activateSelectedCustomizationMenuItem() { if (customizationMenuItems.isEmpty() || currentCustomizationMenuSelection < 0 || currentCustomizationMenuSelection >= customizationMenuItems.size) return val selectedItem = customizationMenuItems[currentCustomizationMenuSelection] gameHaptics.vibrateForPieceLock() when (selectedItem) { is ThemeSelector -> { selectedItem.confirmSelection() Handler(Looper.getMainLooper()).postDelayed({ showCustomizationMenu() }, 100) } is BlockSkinSelector -> { selectedItem.confirmSelection() // The onBlockSkinSelected callback will handle updating the game view and saving the selection } is Switch -> { if (selectedItem.isEnabled) { selectedItem.isChecked = !selectedItem.isChecked } } else -> { selectedItem.performClick() } } } private fun updateGameStateUI(score: Int, level: Int, lines: Int) { viewModel.setScore(score.toLong()) // Use ViewModel setter viewModel.setLevel(level) // Use ViewModel setter // Update other UI elements not handled by score/level observers binding.linesText.text = "$lines" binding.comboText.text = gameBoard.getCombo().toString() // If random mode is enabled, check if we should change theme or block skin if (isRandomModeEnabled) { // Get the current 10-line group (0 for 0-9, 1 for 10-19, etc.) val currentLinesGroup = lines / 10 // Determine if we should change themes and skins: // 1. If we've moved to a new 10-line group // 2. If we've leveled up to a level we haven't applied random to yet val lineGroupChanged = lines > 0 && currentLinesGroup > lastLinesGroup val levelIncreased = level > 1 && level > lastRandomLevel if (lineGroupChanged || levelIncreased) { Log.d("RandomMode", "Triggering change - Lines: $lines (group $currentLinesGroup, was $lastLinesGroup), Level: $level (was $lastRandomLevel)") applyRandomThemeAndBlockSkin() // Update tracking variables lastLinesGroup = currentLinesGroup lastRandomLevel = level } } // Update last lines count lastLines = lines } private fun applyRandomThemeAndBlockSkin() { val unlockedThemes = progressionManager.getUnlockedThemes().toList() val unlockedBlocks = progressionManager.getUnlockedBlocks().toList() // Log available options Log.d("RandomMode", "Available themes: ${unlockedThemes.joinToString()}") Log.d("RandomMode", "Available blocks: ${unlockedBlocks.joinToString()}") // Get current block skin for debugging val currentBlockSkin = gameView.getCurrentBlockSkin() Log.d("RandomMode", "Current block skin before change: $currentBlockSkin") // Only proceed if there are unlocked themes and blocks if (unlockedThemes.isNotEmpty() && unlockedBlocks.isNotEmpty()) { // Apply random theme from unlocked themes - make sure not to pick the current theme val availableThemes = unlockedThemes.filter { it != currentTheme } val randomTheme = if (availableThemes.isNotEmpty()) { availableThemes.random() } else { unlockedThemes.random() } // Apply random block skin from unlocked block skins - make sure not to pick the current skin val availableBlocks = unlockedBlocks.filter { it != currentBlockSkin } val randomBlock = if (availableBlocks.isNotEmpty()) { availableBlocks.random() } else { unlockedBlocks.random() } Log.d("RandomMode", "Applying random theme: $randomTheme and block skin: $randomBlock") // Apply the theme currentTheme = randomTheme applyTheme(randomTheme) // Force update the block skin with a specific call - with intentional delay handler.post { Log.d("RandomMode", "Setting block skin to: $randomBlock") gameView.setBlockSkin(randomBlock) progressionManager.setSelectedBlockSkin(randomBlock) // Get current block skin after change for debugging val newBlockSkin = gameView.getCurrentBlockSkin() Log.d("RandomMode", "Block skin after change: $newBlockSkin") // Update the UI to reflect the changes themeSelector.updateThemes(progressionManager.getUnlockedThemes(), currentTheme) blockSkinSelector.updateBlockSkins( progressionManager.getUnlockedBlocks(), randomBlock, progressionManager.getPlayerLevel() ) // Add a vibration to indicate the change to the player gameHaptics.vibrateForPieceLock() } } else { Log.d("RandomMode", "Cannot apply random theme/skin - no unlocked options available") } } }