diff --git a/app/build.gradle b/app/build.gradle index fece2bd..acd7231 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,7 +19,7 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -47,6 +47,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' + implementation "androidx.activity:activity-ktx:1.9.0" implementation 'androidx.window:window:1.2.0' // For better display support implementation 'androidx.window:window-java:1.2.0' implementation 'com.google.code.gson:gson:2.10.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 72386f6..f3d41de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ - + // Use actual ID from layout - display only the number + binding.scoreText.text = newScore.toString() + }) + + viewModel.currentLevel.observe(this, Observer { newLevel -> + // Use actual ID from layout - display only the number + binding.currentLevelText.text = newLevel.toString() + }) + // Load random mode setting isRandomModeEnabled = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) .getBoolean("random_mode_enabled", false) @@ -348,28 +365,17 @@ class MainActivity : AppCompatActivity(), } gameView.onGameOver = { finalScore -> - // Pause music on game over + // Start animation & pause music + gameView.startGameOverAnimation() gameMusic.pause() - // Update high scores + // Calculate final stats, XP, and high score 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 + statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, viewModel.currentLevel.value ?: 1) val xpGained = progressionManager.calculateGameXP( score = finalScore, linesCleared = gameBoard.lines, - level = currentLevel, + level = viewModel.currentLevel.value ?: 1, gameTime = timePlayedMs, tSpins = statsManager.getSessionTSpins(), combos = statsManager.getSessionMaxCombo(), @@ -377,13 +383,22 @@ class MainActivity : AppCompatActivity(), perfectClearCount = statsManager.getSessionPerfectClears() ) val newRewards = progressionManager.addXP(xpGained) + val newHighScore = highScoreManager.isHighScore(finalScore) + statsManager.endSession() // End session after calculations - // Show progression screen if player earned XP - if (xpGained > 0) { - // Delay showing progression screen for a moment + Log.d(TAG, "Game Over. Score: $finalScore, Level: ${viewModel.currentLevel.value}, Lines: ${gameBoard.lines}, Start Level: $selectedLevel, New High Score: $newHighScore, XP Gained: $xpGained") + + // Show appropriate screen: Progression or Game Over directly + if (xpGained > 0 || newHighScore) { + // Delay showing progression slightly to let animation play Handler(Looper.getMainLooper()).postDelayed({ - showProgressionScreen(xpGained, newRewards) - }, 2000) + 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 } } @@ -515,126 +530,53 @@ class MainActivity : AppCompatActivity(), 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 + * Shows the final game over screen with stats. */ - private fun showGameOver(score: Int) { - Log.d("MainActivity", "Showing game over screen with score: $score") - val gameTime = System.currentTimeMillis() - gameStartTime - - // Hide all game UI elements + private fun showGameOverScreenDirectly(score: Int) { + Log.d(TAG, "Showing final game over screen with score: $score") + // Ensure game UI is hidden binding.gameControlsContainer.visibility = View.GONE binding.holdPieceView.visibility = View.GONE binding.nextPieceView.visibility = View.GONE binding.pauseButton.visibility = View.GONE - - // Hide landscape panels if they exist binding.leftControlsPanel?.visibility = View.GONE binding.rightControlsPanel?.visibility = View.GONE - - // Update session stats - statsManager.updateSessionStats( - score = score, - lines = gameBoard.lines, - pieces = piecesPlaced, - time = gameTime, - level = currentLevel - ) - - // Calculate XP earned - val xpGained = progressionManager.calculateGameXP( - score = statsManager.getSessionScore(), - linesCleared = statsManager.getSessionLines(), - level = currentLevel, - gameTime = gameTime, - tSpins = statsManager.getSessionTSpins(), - combos = statsManager.getSessionMaxCombo(), - quadCount = statsManager.getSessionQuads(), - perfectClearCount = statsManager.getSessionPerfectClears() - ) - - // Add XP and check for rewards - val newRewards = progressionManager.addXP(xpGained) - - // End session and save stats - statsManager.endSession() - - // Update session stats display + progressionScreen.visibility = View.GONE // Ensure progression is hidden + + // Update session stats display in the gameOverContainer val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) timeFormat.timeZone = TimeZone.getTimeZone("UTC") + val gameTime = System.currentTimeMillis() - gameStartTime // Recalculate or pass from onGameOver? + // Let's recalculate for simplicity here. 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) + binding.sessionLevelText.text = getString(R.string.session_level, viewModel.currentLevel.value ?: 1) - // 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.sessionQuadsText.text = getString(R.string.quads, statsManager.getSessionQuads()) - // Flag to track if high score screen will be shown - var showingHighScore = false + // Make the container visible + binding.gameOverContainer.visibility = View.VISIBLE - // Play game over sound and trigger animation - if (isSoundEnabled) { + // Play game over sound if not already played by progression + if (isSoundEnabled && progressionScreen.visibility != View.VISIBLE) { gameMusic.playGameOver() } - - // First trigger the animation in the game view - Log.d("MainActivity", "Triggering game over animation") - gameView.startGameOverAnimation() - - // Wait a moment before showing progression screen to let animation be visible - Handler(Looper.getMainLooper()).postDelayed({ - // Show progression screen first with XP animation - showProgressionScreen(xpGained, newRewards) - - // Override the continue button behavior if high score needs to be shown - val originalOnContinue = progressionScreen.onContinue - - progressionScreen.onContinue = { - // If this is a high score, show high score entry screen - if (highScoreManager.isHighScore(score)) { - showingHighScore = true - showHighScoreEntry(score) - } else { - // Just show game over screen normally - progressionScreen.visibility = View.GONE - binding.gameOverContainer.visibility = View.VISIBLE - - // Keep all game UI elements hidden - binding.gameControlsContainer.visibility = View.GONE - binding.holdPieceView.visibility = View.GONE - binding.nextPieceView.visibility = View.GONE - binding.pauseButton.visibility = View.GONE - binding.leftControlsPanel?.visibility = View.GONE - binding.rightControlsPanel?.visibility = View.GONE - - // Get all themes with "Theme" in their name - val themeRewards = progressionManager.getUnlockedThemes().filter { - it.contains("Theme", ignoreCase = true) - } - - // Update theme selector if new themes were unlocked - if (themeRewards.isNotEmpty()) { - updateThemeSelector() - } - } - } - }, 2000) // Increased from 1000ms (1 second) to 2000ms (2 seconds) - - // Vibrate to indicate game over - vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK) + + // Vibrate if not already vibrated by progression + if (progressionScreen.visibility != View.VISIBLE) { + vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK) + } } /** @@ -643,7 +585,7 @@ class MainActivity : AppCompatActivity(), private fun showHighScoreEntry(score: Int) { val intent = Intent(this, HighScoreEntryActivity::class.java).apply { putExtra("score", score) - putExtra("level", currentLevel) + putExtra("level", viewModel.currentLevel.value ?: 1) // Read from ViewModel } // Use the launcher instead of startActivity highScoreEntryLauncher.launch(intent) @@ -930,30 +872,23 @@ class MainActivity : AppCompatActivity(), * Start a new game */ private fun startGame() { - // Reset pieces placed counter + Log.d(TAG, "Starting game at level $selectedLevel") + // Reset game state + viewModel.resetGame() // Resets score and level in ViewModel + viewModel.setLevel(selectedLevel) // Set initial level from selection piecesPlaced = 0 - - // Set initial game state - currentScore = 0 - currentLevel = selectedLevel - lastLines = 0 // Reset lastLines to 0 - lastLinesGroup = 0 // Reset lastLinesGroup to 0 - lastRandomLevel = 0 // Reset lastRandomLevel to 0 gameStartTime = System.currentTimeMillis() - - // Update UI to show initial values - binding.scoreText.text = "$currentScore" - binding.currentLevelText.text = "$currentLevel" - binding.linesText.text = "0" - binding.comboText.text = "0" - + 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 (helps with random mode) + // Ensure block skin is properly set val selectedSkin = progressionManager.getSelectedBlockSkin() - Log.d("RandomMode", "Game start: Setting block skin to $selectedSkin") gameView.setBlockSkin(selectedSkin) // Update selectors to refresh UI state @@ -962,9 +897,9 @@ class MainActivity : AppCompatActivity(), selectedSkin, progressionManager.getPlayerLevel() ) - - // Show game elements - gameView.visibility = View.VISIBLE + + // 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 @@ -974,7 +909,7 @@ class MainActivity : AppCompatActivity(), titleScreen.visibility = View.GONE progressionScreen.visibility = View.GONE - // Show game UI elements in landscape mode + // 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 @@ -982,32 +917,22 @@ class MainActivity : AppCompatActivity(), // Configure callbacks gameView.onGameStateChanged = { score, level, lines -> - updateGameStateUI(score, level, lines) + // We'll adapt updateGameStateUI later to use ViewModel + updateGameStateUI(score, level, lines) } gameView.onGameOver = { finalScore -> - // Pause music on game over + // Start animation & pause music + gameView.startGameOverAnimation() gameMusic.pause() - // Update high scores + // Calculate final stats, XP, and high score 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 + statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, viewModel.currentLevel.value ?: 1) val xpGained = progressionManager.calculateGameXP( score = finalScore, linesCleared = gameBoard.lines, - level = currentLevel, + level = viewModel.currentLevel.value ?: 1, gameTime = timePlayedMs, tSpins = statsManager.getSessionTSpins(), combos = statsManager.getSessionMaxCombo(), @@ -1015,13 +940,22 @@ class MainActivity : AppCompatActivity(), perfectClearCount = statsManager.getSessionPerfectClears() ) val newRewards = progressionManager.addXP(xpGained) + val newHighScore = highScoreManager.isHighScore(finalScore) + statsManager.endSession() // End session after calculations - // Show progression screen if player earned XP - if (xpGained > 0) { - // Delay showing progression screen for a moment + Log.d(TAG, "Game Over. Score: $finalScore, Level: ${viewModel.currentLevel.value}, Lines: ${gameBoard.lines}, Start Level: $selectedLevel, New High Score: $newHighScore, XP Gained: $xpGained") + + // Show appropriate screen: Progression or Game Over directly + if (xpGained > 0 || newHighScore) { + // Delay showing progression slightly to let animation play Handler(Looper.getMainLooper()).postDelayed({ - showProgressionScreen(xpGained, newRewards) - }, 2000) + 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 } } @@ -1423,11 +1357,11 @@ class MainActivity : AppCompatActivity(), return super.onGenericMotionEvent(event) } - private fun showProgressionScreen(xpGained: Long, newRewards: List) { + private fun showProgressionScreen(xpGained: Long, newRewards: List, isNewHighScore: Boolean, finalScore: Int) { // Apply theme before showing the screen progressionScreen.applyTheme(currentTheme) - // Hide all game UI elements + // Hide game/other UI elements binding.gameControlsContainer.visibility = View.GONE binding.holdPieceView.visibility = View.GONE binding.nextPieceView.visibility = View.GONE @@ -1443,6 +1377,16 @@ class MainActivity : AppCompatActivity(), // 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) + } + } } /** @@ -2045,11 +1989,10 @@ class MainActivity : AppCompatActivity(), } private fun updateGameStateUI(score: Int, level: Int, lines: Int) { - currentScore = score.toLong() - currentLevel = level + viewModel.setScore(score.toLong()) // Use ViewModel setter + viewModel.setLevel(level) // Use ViewModel setter - binding.scoreText.text = "$score" - binding.currentLevelText.text = "$level" + // Update other UI elements not handled by score/level observers binding.linesText.text = "$lines" binding.comboText.text = gameBoard.getCombo().toString() diff --git a/app/src/main/java/com/pixelmintdrop/MainActivityViewModel.kt b/app/src/main/java/com/pixelmintdrop/MainActivityViewModel.kt new file mode 100644 index 0000000..e62a138 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/MainActivityViewModel.kt @@ -0,0 +1,40 @@ +package com.pixelmintdrop + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class MainActivityViewModel : ViewModel() { + + // Private MutableLiveData for internal updates + private val _currentScore = MutableLiveData(0L) + private val _currentLevel = MutableLiveData(1) + + // Public LiveData for observation by the Activity + val currentScore: LiveData = _currentScore + val currentLevel: LiveData = _currentLevel + + // Example function to update the score (logic would be moved here) + fun incrementScore(points: Long) { + _currentScore.value = (_currentScore.value ?: 0L) + points + // Potentially add logic here to check for level up based on score + } + + // Function to set the score directly + fun setScore(score: Long) { + _currentScore.value = score + } + + // Example function to update the level + fun setLevel(level: Int) { + _currentLevel.value = level + } + + fun resetGame() { + _currentScore.value = 0L + _currentLevel.value = 1 + // Reset other game state within the ViewModel as needed + } + + // Add other state variables and logic related to game state here +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8..c1d5e01 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME