mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-17 21:35:20 +01:00
2209 lines
No EOL
95 KiB
Kotlin
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")
|
|
}
|
|
}
|
|
} |