mintris/app/src/main/java/com/pixelmintdrop/MainActivity.kt

2209 lines
No EOL
95 KiB
Kotlin

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 piecesPlaced = 0
private var gameStartTime = 0L
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 currentTheme = PlayerProgressionManager.THEME_CLASSIC
private val handler = Handler(Looper.getMainLooper())
// Activity result launcher for high score entry
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
// 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<String>()
// Track currently selected menu item in pause menu for gamepad navigation
private var currentMenuSelection = 0
private val pauseMenuItems = mutableListOf<View>()
private val customizationMenuItems = mutableListOf<View>()
// Add these new properties at the class level
private var currentCustomizationMenuSelection = 0
// CAPTURE session stats needed for display into MainActivity member variables
private var lastSessionScore = 0
private var lastSessionLines = 0
private var lastSessionPieces = 0
private var lastSessionTime = 0L
private var lastSessionLevel = 0
private var lastSessionSingles = 0
private var lastSessionDoubles = 0
private var lastSessionTriples = 0
private var lastSessionQuads = 0
override fun onCreate(savedInstanceState: Bundle?) {
// Register activity result launcher for high score entry
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// When returning from HighScoreEntryActivity:
// 1. Hide progression screen
progressionScreen.visibility = View.GONE
// 2. Make game over container visible (it's already laid out)
binding.gameOverContainer.visibility = View.VISIBLE
// 3. *** Call showGameOverScreenDirectly to populate the stats ***
showGameOverScreenDirectly(lastSessionScore) // Use the captured score
// 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()
})
// Observe Sound/Music state
viewModel.isSoundEnabled.observe(this, Observer { enabled ->
updateSoundToggleUI(enabled)
})
viewModel.isMusicEnabled.observe(this, Observer { enabled ->
updateMusicToggleUI(enabled)
// Also update GameMusic immediately
gameMusic.setEnabled(enabled)
})
// Observe Random Mode state
viewModel.isRandomModeEnabled.observe(this, Observer { enabled ->
// Update the switch UI
binding.randomModeSwitch?.isChecked = enabled
})
// 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 globally
applyTheme(themeId)
// Provide haptic feedback as a cue that the theme changed
gameHaptics.vibrateForPieceLock()
// --- Re-apply theme to the currently visible Customization Menu ---
val themeColor = getThemeColor(themeId)
val backgroundColor = getThemeBackgroundColor(themeId)
binding.customizationContainer.setBackgroundColor(backgroundColor)
binding.customizationLevelBadge.setThemeColor(themeColor)
applyThemeColorToTextViews(binding.customizationContainer, themeColor)
// ------------------------------------------------------------------
// // Refreshing pause menu here is not needed for instant update of customization menu
// 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
// Hide landscape control panels if in landscape mode and title screen is visible
if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
binding.leftControlsPanel?.visibility = View.GONE
binding.rightControlsPanel?.visibility = View.GONE
}
// 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 {
viewModel.toggleMusic() // Use ViewModel
// Observer will call updateMusicToggleUI and gameMusic.setEnabled
}
// Set up callbacks
gameView.onGameStateChanged = { score, level, lines ->
updateGameStateUI(score, level, lines)
}
gameView.onGameOver = { finalScore ->
// Start animation & pause music & play sound
gameView.startGameOverAnimation()
gameMusic.pause()
if (viewModel.isSoundEnabled.value == true) { // Play sound if enabled
gameMusic.playGameOver()
}
// 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
// CAPTURE session stats needed for display into MainActivity member variables
lastSessionScore = statsManager.getSessionScore()
lastSessionLines = statsManager.getSessionLines()
lastSessionPieces = statsManager.getSessionPieces()
lastSessionTime = statsManager.getSessionTime()
lastSessionLevel = statsManager.getSessionLevel()
lastSessionSingles = statsManager.getSessionSingles()
lastSessionDoubles = statsManager.getSessionDoubles()
lastSessionTriples = statsManager.getSessionTriples()
lastSessionQuads = statsManager.getSessionQuads()
// *** Add detailed logging here ***
Log.d(TAG, "[GameOverDebug] Captured Stats:")
Log.d(TAG, "[GameOverDebug] Score: $lastSessionScore (from manager: ${statsManager.getSessionScore()})")
Log.d(TAG, "[GameOverDebug] Lines: $lastSessionLines (from manager: ${statsManager.getSessionLines()})")
Log.d(TAG, "[GameOverDebug] Pieces: $lastSessionPieces (from manager: ${statsManager.getSessionPieces()})")
Log.d(TAG, "[GameOverDebug] Time: $lastSessionTime (from manager: ${statsManager.getSessionTime()})")
Log.d(TAG, "[GameOverDebug] Level: $lastSessionLevel (from manager: ${statsManager.getSessionLevel()})")
Log.d(TAG, "[GameOverDebug] Singles: $lastSessionSingles (from manager: ${statsManager.getSessionSingles()})")
Log.d(TAG, "[GameOverDebug] Doubles: $lastSessionDoubles (from manager: ${statsManager.getSessionDoubles()})")
Log.d(TAG, "[GameOverDebug] Triples: $lastSessionTriples (from manager: ${statsManager.getSessionTriples()})")
Log.d(TAG, "[GameOverDebug] Quads: $lastSessionQuads (from manager: ${statsManager.getSessionQuads()})")
// End the session (updates lifetime stats)
statsManager.endSession()
Log.d(TAG, "Game Over. Captured Score: $lastSessionScore, Level: $lastSessionLevel, Lines: $lastSessionLines, Start Level: ${viewModel.selectedLevel.value}, 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 (viewModel.isSoundEnabled.value == true) { // Read from ViewModel
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 (viewModel.isSoundEnabled.value == true) { // Read from ViewModel
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)
viewModel.toggleSound() // Use ViewModel
// Observer will call updateSoundToggleUI
}
// 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)
val currentSelected = viewModel.selectedLevel.value ?: 1
if (currentSelected < maxLevel) {
viewModel.setSelectedLevel(currentSelected + 1)
updateLevelSelector()
}
}
binding.pauseLevelDownButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
val currentSelected = viewModel.selectedLevel.value ?: 1
if (currentSelected > 1) {
viewModel.setSelectedLevel(currentSelected - 1)
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()
// Set up random mode switch
binding.randomModeSwitch?.apply {
isChecked = viewModel.isRandomModeEnabled.value ?: false
isEnabled = progressionManager.getPlayerLevel() >= 5
setOnCheckedChangeListener { _, isChecked ->
// Only need to call toggle, ViewModel handles saving
viewModel.toggleRandomMode()
}
}
}
/**
* 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) { // Keep score param for logging if needed elsewhere
Log.d(TAG, "Showing final game over screen with score from param: $score, from StatsManager: ${statsManager.getSessionScore()}")
// 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 // No longer needed here
// --- Consistently use StatsManager session getters ---
binding.sessionScoreText.text = "Score: ${statsManager.getSessionScore()}"
binding.sessionLinesText.text = "Lines: ${statsManager.getSessionLines()}"
binding.sessionPiecesText.text = "Pieces: ${statsManager.getSessionPieces()}"
binding.sessionTimeText.text = "Time: ${timeFormat.format(statsManager.getSessionTime())}"
binding.sessionLevelText.text = "Level: ${statsManager.getSessionLevel()}"
binding.sessionSinglesText.text = "Singles: ${statsManager.getSessionSingles()}"
binding.sessionDoublesText.text = "Doubles: ${statsManager.getSessionDoubles()}"
binding.sessionTriplesText.text = "Triples: ${statsManager.getSessionTriples()}"
binding.sessionQuadsText.text = "Quads: ${statsManager.getSessionQuads()}"
// --- End StatsManager usage ---
// Make the container visible
binding.gameOverContainer.visibility = View.VISIBLE
// 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 = viewModel.isRandomModeEnabled.value ?: false
isEnabled = progressionManager.getPlayerLevel() >= 5
setOnCheckedChangeListener { _, isChecked ->
// Only need to call toggle, ViewModel handles saving
viewModel.toggleRandomMode()
}
}
}
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
// Refresh the pause menu to apply theme changes
showPauseMenu()
}
/**
* 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
}
/**
* Update the level selector display
*/
private fun updateLevelSelector() {
binding.pauseLevelText.text = viewModel.selectedLevel.value.toString()
gameBoard.updateLevel(viewModel.selectedLevel.value ?: 1)
}
/**
* 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))
}
}
/**
* Update the sound toggle button text
*/
private fun updateSoundToggleUI(enabled: Boolean) {
binding.settingsButton.text = getString(
if (enabled) R.string.sound_on else R.string.sound_off
)
}
/**
* Update the music toggle button icon
*/
private fun updateMusicToggleUI(enabled: Boolean) {
binding.musicToggle.setImageResource(
if (enabled) 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 ${viewModel.selectedLevel.value}")
// Reset session stats FIRST
statsManager.startNewSession()
// Reset game state
viewModel.resetGame() // Resets score and level in ViewModel
viewModel.setLevel(viewModel.selectedLevel.value ?: 1) // 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 & play sound
gameView.startGameOverAnimation()
gameMusic.pause()
if (viewModel.isSoundEnabled.value == true) { // Play sound if enabled
gameMusic.playGameOver()
}
// 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
// CAPTURE session stats needed for display into MainActivity member variables
lastSessionScore = statsManager.getSessionScore()
lastSessionLines = statsManager.getSessionLines()
lastSessionPieces = statsManager.getSessionPieces()
lastSessionTime = statsManager.getSessionTime()
lastSessionLevel = statsManager.getSessionLevel()
lastSessionSingles = statsManager.getSessionSingles()
lastSessionDoubles = statsManager.getSessionDoubles()
lastSessionTriples = statsManager.getSessionTriples()
lastSessionQuads = statsManager.getSessionQuads()
// *** Add detailed logging here ***
Log.d(TAG, "[GameOverDebug] Captured Stats:")
Log.d(TAG, "[GameOverDebug] Score: $lastSessionScore (from manager: ${statsManager.getSessionScore()})")
Log.d(TAG, "[GameOverDebug] Lines: $lastSessionLines (from manager: ${statsManager.getSessionLines()})")
Log.d(TAG, "[GameOverDebug] Pieces: $lastSessionPieces (from manager: ${statsManager.getSessionPieces()})")
Log.d(TAG, "[GameOverDebug] Time: $lastSessionTime (from manager: ${statsManager.getSessionTime()})")
Log.d(TAG, "[GameOverDebug] Level: $lastSessionLevel (from manager: ${statsManager.getSessionLevel()})")
Log.d(TAG, "[GameOverDebug] Singles: $lastSessionSingles (from manager: ${statsManager.getSessionSingles()})")
Log.d(TAG, "[GameOverDebug] Doubles: $lastSessionDoubles (from manager: ${statsManager.getSessionDoubles()})")
Log.d(TAG, "[GameOverDebug] Triples: $lastSessionTriples (from manager: ${statsManager.getSessionTriples()})")
Log.d(TAG, "[GameOverDebug] Quads: $lastSessionQuads (from manager: ${statsManager.getSessionQuads()})")
// End the session (updates lifetime stats)
statsManager.endSession()
Log.d(TAG, "Game Over. Captured Score: $lastSessionScore, Level: $lastSessionLevel, Lines: $lastSessionLines, Start Level: ${viewModel.selectedLevel.value}, 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 (viewModel.isMusicEnabled.value == true) { // Read from ViewModel
gameMusic.prepareMusic() // Ensure player is ready
gameMusic.start()
}
// Start the game
gameView.start()
// Observer ensures gameMusic is enabled/disabled correctly via gameMusic.setEnabled()
// Reset session stats - MOVED TO TOP
// statsManager.startNewSession()
progressionManager.startNewSession()
gameBoard.updateLevel(viewModel.selectedLevel.value ?: 1)
}
private fun restartGame() {
gameBoard.reset()
gameView.visibility = View.VISIBLE
gameView.start()
showPauseMenu()
}
private fun resumeGame() {
gameView.resume()
if (viewModel.isMusicEnabled.value == true) { // Read from ViewModel
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()
// Update visibility of control panels in landscape orientation based on title screen visibility
if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
if (titleScreen.visibility == View.VISIBLE) {
// Hide control panels when title screen is visible
binding.leftControlsPanel?.visibility = View.GONE
binding.rightControlsPanel?.visibility = View.GONE
} else if (gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE
&& binding.pauseContainer.visibility == View.GONE) {
// Show control panels when game is active
binding.leftControlsPanel?.visibility = View.VISIBLE
binding.rightControlsPanel?.visibility = View.VISIBLE
}
}
// 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.customizationContainer.visibility == View.VISIBLE) {
// If customization menu is showing, hide it
hideCustomizationMenu()
} 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<View>(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<String>, 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<View?>()
// 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<View>(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 (viewModel.isRandomModeEnabled.value == true) {
// 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")
}
}
}