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

2027 lines
No EOL
82 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.view.View
import android.view.HapticFeedbackConstants
import androidx.appcompat.app.AppCompatActivity
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
class MainActivity : AppCompatActivity(),
GamepadController.GamepadConnectionListener,
GamepadController.GamepadMenuListener,
GamepadController.GamepadNavigationListener {
companion object {
private const val TAG = "MainActivity"
}
// 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 currentScore = 0L
private var currentLevel = 1
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<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
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)
// 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
// 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)
// 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 ->
// Pause music on game over
gameMusic.pause()
// Update high scores
val timePlayedMs = System.currentTimeMillis() - gameStartTime
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val currentDate = dateFormat.format(Date())
// Track if this is a high score
val isHighScore = highScoreManager.isHighScore(finalScore)
// Show game over screen
showGameOver(finalScore)
// Save player stats to track game history
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
// Handle progression - XP earned, potential level up
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
val newRewards = progressionManager.addXP(xpGained)
// Show progression screen if player earned XP
if (xpGained > 0) {
// Delay showing progression screen for a moment
Handler(Looper.getMainLooper()).postDelayed({
showProgressionScreen(xpGained, newRewards)
}, 2000)
}
}
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) {
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()
// Update current level for stats
currentLevel = level
// Force redraw of next piece preview
binding.nextPieceView.invalidate()
}
/**
* Show game over screen
*/
private fun showGameOver(score: Int) {
Log.d("MainActivity", "Showing game over screen with score: $score")
val gameTime = System.currentTimeMillis() - gameStartTime
// Hide all game 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
// Update session stats
statsManager.updateSessionStats(
score = score,
lines = gameBoard.lines,
pieces = piecesPlaced,
time = gameTime,
level = currentLevel
)
// Calculate XP earned
val xpGained = progressionManager.calculateGameXP(
score = score,
lines = gameBoard.lines,
level = currentLevel,
gameTime = gameTime,
tetrisCount = statsManager.getSessionTetrises(),
perfectClearCount = 0 // Implement perfect clear tracking if needed
)
// Add XP and check for rewards
val newRewards = progressionManager.addXP(xpGained)
// End session and save stats
statsManager.endSession()
// Update session stats display
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timeFormat.timeZone = TimeZone.getTimeZone("UTC")
binding.sessionScoreText.text = getString(R.string.session_score, score)
binding.sessionLinesText.text = getString(R.string.session_lines, gameBoard.lines)
binding.sessionPiecesText.text = getString(R.string.session_pieces, piecesPlaced)
binding.sessionTimeText.text = getString(R.string.session_time, timeFormat.format(gameTime))
binding.sessionLevelText.text = getString(R.string.session_level, currentLevel)
// Update session line clear stats
binding.sessionSinglesText.text = getString(R.string.singles, statsManager.getSessionSingles())
binding.sessionDoublesText.text = getString(R.string.doubles, statsManager.getSessionDoubles())
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
// Flag to track if high score screen will be shown
var showingHighScore = false
// Play game over sound and trigger animation
if (isSoundEnabled) {
gameMusic.playGameOver()
}
// First trigger the animation in the game view
Log.d("MainActivity", "Triggering game over animation")
gameView.startGameOverAnimation()
// Wait a moment before showing progression screen to let animation be visible
Handler(Looper.getMainLooper()).postDelayed({
// Show progression screen first with XP animation
showProgressionScreen(xpGained, newRewards)
// Override the continue button behavior if high score needs to be shown
val originalOnContinue = progressionScreen.onContinue
progressionScreen.onContinue = {
// If this is a high score, show high score entry screen
if (highScoreManager.isHighScore(score)) {
showingHighScore = true
showHighScoreEntry(score)
} else {
// Just show game over screen normally
progressionScreen.visibility = View.GONE
binding.gameOverContainer.visibility = View.VISIBLE
// 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()
}
}
}
}, 2000) // Increased from 1000ms (1 second) to 2000ms (2 seconds)
// Vibrate to indicate game over
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", currentLevel)
}
// 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) {
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() {
// Reset pieces placed counter
piecesPlaced = 0
// Set initial game state
currentScore = 0
currentLevel = selectedLevel
lastLines = 0 // Reset lastLines to 0
lastLinesGroup = 0 // Reset lastLinesGroup to 0
lastRandomLevel = 0 // Reset lastRandomLevel to 0
gameStartTime = System.currentTimeMillis()
// Update UI to show initial values
binding.scoreText.text = "$currentScore"
binding.currentLevelText.text = "$currentLevel"
binding.linesText.text = "0"
binding.comboText.text = "0"
// Reset game view and game board
gameBoard.reset()
gameView.reset()
// Ensure block skin is properly set (helps with random mode)
val selectedSkin = progressionManager.getSelectedBlockSkin()
Log.d("RandomMode", "Game start: Setting block skin to $selectedSkin")
gameView.setBlockSkin(selectedSkin)
// Update selectors to refresh UI state
blockSkinSelector.updateBlockSkins(
progressionManager.getUnlockedBlocks(),
selectedSkin,
progressionManager.getPlayerLevel()
)
// Show game elements
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 game UI elements in landscape mode
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 ->
updateGameStateUI(score, level, lines)
}
gameView.onGameOver = { finalScore ->
// Pause music on game over
gameMusic.pause()
// Update high scores
val timePlayedMs = System.currentTimeMillis() - gameStartTime
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val currentDate = dateFormat.format(Date())
// Track if this is a high score
val isHighScore = highScoreManager.isHighScore(finalScore)
// Show game over screen
showGameOver(finalScore)
// Save player stats to track game history
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
// Handle progression - XP earned, potential level up
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
val newRewards = progressionManager.addXP(xpGained)
// Show progression screen if player earned XP
if (xpGained > 0) {
// Delay showing progression screen for a moment
Handler(Looper.getMainLooper()).postDelayed({
showProgressionScreen(xpGained, newRewards)
}, 2000)
}
}
// 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
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<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>) {
// Apply theme before showing the screen
progressionScreen.applyTheme(currentTheme)
// Hide all game 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)
}
/**
* 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) {
currentScore = score.toLong()
currentLevel = level
binding.scoreText.text = "$score"
binding.currentLevelText.text = "$level"
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")
}
}
}