package com.mintris 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.mintris.databinding.ActivityMainBinding import com.mintris.game.GameHaptics import com.mintris.game.GameView import com.mintris.game.TitleScreen import com.mintris.model.GameBoard import com.mintris.audio.GameMusic import com.mintris.model.HighScoreManager import com.mintris.model.PlayerProgressionManager import com.mintris.model.StatsManager import com.mintris.ui.ProgressionScreen import com.mintris.ui.ThemeSelector import com.mintris.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.mintris.game.GamepadController import android.view.InputDevice import android.widget.Toast import android.content.BroadcastReceiver import android.content.IntentFilter import android.app.AlertDialog 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 // Game state private var isSoundEnabled = true private var isMusicEnabled = true private var selectedLevel = 1 private val maxLevel = 20 private var currentScore = 0 private var currentLevel = 1 private var gameStartTime: Long = 0 private var piecesPlaced: Int = 0 private var currentTheme = PlayerProgressionManager.THEME_CLASSIC // Activity result launcher for high score entry private lateinit var highScoreEntryLauncher: ActivityResultLauncher // Gamepad controller private lateinit var gamepadController: GamepadController // Input device change receiver private val inputDeviceReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { Intent.ACTION_CONFIGURATION_CHANGED -> { // A gamepad might have connected or disconnected checkGamepadConnections() } } } } // Track connected gamepads to detect changes private val connectedGamepads = mutableSetOf() // Track currently selected menu item in pause menu for gamepad navigation private var currentMenuSelection = 0 private val pauseMenuItems = mutableListOf() 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 } 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.themeSelector blockSkinSelector = binding.blockSkinSelector // 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 } // 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 landscape mode theme selector if available val inPauseThemeSelector = findViewById(R.id.inPauseThemeSelector) inPauseThemeSelector?.onThemeSelected = { themeId: String -> // Apply the new theme applyTheme(themeId) // Provide haptic feedback gameHaptics.vibrateForPieceLock() // Refresh the pause menu showPauseMenu() } // Set up block skin selector blockSkinSelector.onBlockSkinSelected = { skinId: String -> // Apply the new block skin gameView.setBlockSkin(skinId) // Save the selection progressionManager.setSelectedBlockSkin(skinId) // Provide haptic feedback gameHaptics.vibrateForPieceLock() } // Set up landscape mode block skin selector if available val inPauseBlockSkinSelector = findViewById(R.id.inPauseBlockSkinSelector) inPauseBlockSkinSelector?.onBlockSkinSelected = { skinId: String -> // Apply the new block skin gameView.setBlockSkin(skinId) // Save the selection progressionManager.setSelectedBlockSkin(skinId) // Provide haptic feedback gameHaptics.vibrateForPieceLock() } // 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() } // Set up music toggle binding.musicToggle.setOnClickListener { isMusicEnabled = !isMusicEnabled gameMusic.setEnabled(isMusicEnabled) updateMusicToggleUI() } // Set up callbacks gameView.onGameStateChanged = { score, level, lines -> updateUI(score, level, lines) } // Track pieces placed using callback gameBoard.onPieceLock = { // Increment pieces placed counter piecesPlaced++ // Provide haptic feedback gameHaptics.vibrateForPieceLock() } 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) } // 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 // 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() // Hide game UI elements in landscape mode if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { binding.leftControlsPanel?.visibility = View.GONE binding.rightControlsPanel?.visibility = View.GONE } // 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 // Update theme selector if new themes were unlocked if (newRewards.any { it.contains("Theme") }) { 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 settings menu */ private fun showPauseMenu() { binding.pauseContainer.visibility = View.VISIBLE binding.pauseStartButton.visibility = View.VISIBLE binding.resumeButton.visibility = View.GONE // Update level badge binding.pauseLevelBadge?.setLevel(progressionManager.getPlayerLevel()) binding.pauseLevelBadge?.setThemeColor(getThemeColor(currentTheme)) // Get theme color val textColor = getThemeColor(currentTheme) // Apply theme color to pause container background val backgroundColor = when (currentTheme) { PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221") PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A") PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832") PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10") else -> Color.BLACK } binding.pauseContainer.setBackgroundColor(backgroundColor) // Apply theme colors to buttons binding.pauseStartButton.setTextColor(textColor) binding.pauseRestartButton.setTextColor(textColor) binding.resumeButton.setTextColor(textColor) binding.highScoresButton.setTextColor(textColor) binding.statsButton.setTextColor(textColor) binding.pauseLevelText?.setTextColor(textColor) binding.pauseLevelUpButton.setTextColor(textColor) binding.pauseLevelDownButton.setTextColor(textColor) binding.settingsButton.setTextColor(textColor) binding.musicToggle.setColorFilter(textColor) // Apply theme colors to text elements binding.settingsTitle?.setTextColor(textColor) binding.selectLevelText.setTextColor(textColor) binding.musicText.setTextColor(textColor) // Update theme selector - handle both standard and landscape versions updateThemeSelector() // Handle landscape mode theme selectors (using null-safe calls) val inPauseThemeSelector = findViewById(R.id.inPauseThemeSelector) inPauseThemeSelector?.updateThemes( unlockedThemes = progressionManager.getUnlockedThemes(), currentTheme = currentTheme ) // Update block skin selector - handle both standard and landscape versions blockSkinSelector.updateBlockSkins( progressionManager.getUnlockedBlocks(), gameView.getCurrentBlockSkin(), progressionManager.getPlayerLevel() ) // Handle landscape mode block skin selectors (using null-safe calls) val inPauseBlockSkinSelector = findViewById(R.id.inPauseBlockSkinSelector) inPauseBlockSkinSelector?.updateBlockSkins( progressionManager.getUnlockedBlocks(), gameView.getCurrentBlockSkin(), progressionManager.getPlayerLevel() ) // Initialize pause menu navigation initPauseMenuNavigation() // Reset and highlight first selectable menu item if (pauseMenuItems.isNotEmpty()) { currentMenuSelection = if (binding.resumeButton.visibility == View.VISIBLE) 0 else 1 highlightMenuItem(currentMenuSelection) } } /** * 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 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() // Show game elements gameView.visibility = View.VISIBLE binding.gameControlsContainer.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 -> currentScore = score currentLevel = level binding.scoreText.text = "$score" binding.currentLevelText.text = "$level" binding.linesText.text = "$lines" binding.comboText.text = gameBoard.getCombo().toString() } 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.themeSelector.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(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.mintris.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.mintris.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) { // Handle back button press as a pause action during gameplay if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) { gameView.pause() gameMusic.pause() showPauseMenu() binding.pauseStartButton.visibility = View.GONE binding.resumeButton.visibility = View.VISIBLE return true // Consume the event } else if (binding.pauseContainer.visibility == View.VISIBLE) { // If pause menu is showing, handle as a resume resumeGame() return true // Consume the event } else if (binding.gameOverContainer.visibility == View.VISIBLE) { // If game over is showing, go back to title hideGameOver() showTitleScreen() return true // Consume the event } } return super.onKeyDown(keyCode, event) } /** * 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) { // Apply theme before showing the screen progressionScreen.applyTheme(currentTheme) // 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 (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() } } } override fun onMenuDown() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE) { moveMenuSelectionDown() } } } override fun onMenuSelect() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE) { activateSelectedMenuItem() } else if (titleScreen.visibility == View.VISIBLE) { titleScreen.performClick() } else if (progressionScreen.visibility == View.VISIBLE) { // Handle continue in progression screen progressionScreen.performContinue() } else if (binding.gameOverContainer.visibility == View.VISIBLE) { // Handle play again in game over screen binding.playAgainButton.performClick() } } } /** * Initialize pause menu items for gamepad navigation */ private fun initPauseMenuNavigation() { pauseMenuItems.clear() pauseMenuItems.add(binding.resumeButton) pauseMenuItems.add(binding.pauseStartButton) pauseMenuItems.add(binding.pauseRestartButton) pauseMenuItems.add(binding.highScoresButton) pauseMenuItems.add(binding.statsButton) pauseMenuItems.add(binding.pauseLevelUpButton) pauseMenuItems.add(binding.pauseLevelDownButton) pauseMenuItems.add(binding.settingsButton) pauseMenuItems.add(binding.musicToggle) // Add theme selector if present val themeSelector = binding.themeSelector if (themeSelector.visibility == View.VISIBLE) { pauseMenuItems.add(themeSelector) } // Add block skin selector if present val blockSkinSelector = binding.blockSkinSelector if (blockSkinSelector.visibility == View.VISIBLE) { pauseMenuItems.add(blockSkinSelector) } } /** * Highlight the currently selected menu item */ private fun highlightMenuItem(index: Int) { // Reset all items to normal state pauseMenuItems.forEachIndexed { i, item -> item.alpha = if (i == index) 1.0f else 0.7f item.scaleX = if (i == index) 1.2f else 1.0f item.scaleY = if (i == index) 1.2f else 1.0f } } /** * Move menu selection up */ private fun moveMenuSelectionUp() { if (pauseMenuItems.isEmpty()) return currentMenuSelection = (currentMenuSelection - 1 + pauseMenuItems.size) % pauseMenuItems.size highlightMenuItem(currentMenuSelection) gameHaptics.vibrateForPieceMove() } /** * Move menu selection down */ private fun moveMenuSelectionDown() { if (pauseMenuItems.isEmpty()) return currentMenuSelection = (currentMenuSelection + 1) % pauseMenuItems.size highlightMenuItem(currentMenuSelection) gameHaptics.vibrateForPieceMove() } /** * Activate the currently selected menu item */ private fun activateSelectedMenuItem() { if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return pauseMenuItems[currentMenuSelection].performClick() } }