diff --git a/app/src/main/java/com/mintris/HighScoreEntryActivity.kt b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt index ef2e13a..313685b 100644 --- a/app/src/main/java/com/mintris/HighScoreEntryActivity.kt +++ b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt @@ -1,5 +1,6 @@ package com.mintris +import android.app.Activity import android.os.Bundle import android.widget.Button import android.widget.EditText @@ -13,6 +14,9 @@ class HighScoreEntryActivity : AppCompatActivity() { private lateinit var nameInput: EditText private lateinit var scoreText: TextView private lateinit var saveButton: Button + + // Track if we already saved to prevent double-saving + private var hasSaved = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,12 +32,28 @@ class HighScoreEntryActivity : AppCompatActivity() { scoreText.text = getString(R.string.score) + ": $score" saveButton.setOnClickListener { - val name = nameInput.text.toString().trim() - if (name.isNotEmpty()) { - val highScore = HighScore(name, score, level) - highScoreManager.addHighScore(highScore) - finish() + // Only allow saving once + if (!hasSaved) { + val name = nameInput.text.toString().trim() + if (name.isNotEmpty()) { + hasSaved = true + val highScore = HighScore(name, score, level) + highScoreManager.addHighScore(highScore) + + // Set result and finish + setResult(Activity.RESULT_OK) + finish() + } } } } + + // Prevent accidental back button press from causing issues + override fun onBackPressed() { + // If they haven't saved yet, consider it a cancel + if (!hasSaved) { + setResult(Activity.RESULT_CANCELED) + } + finish() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index fd49615..83fdd6c 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -1,6 +1,9 @@ package com.mintris +import android.animation.ObjectAnimator +import android.animation.ValueAnimator import android.content.Context +import android.content.Intent import android.os.Build import android.os.Bundle import android.os.VibrationEffect @@ -11,6 +14,7 @@ import android.widget.Button import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import com.mintris.databinding.ActivityMainBinding import com.mintris.game.GameHaptics import com.mintris.game.GameView @@ -20,10 +24,14 @@ import android.view.HapticFeedbackConstants import com.mintris.model.GameBoard import com.mintris.audio.GameMusic import com.mintris.model.HighScoreManager -import android.content.Intent +import com.mintris.model.PlayerProgressionManager import com.mintris.model.StatsManager +import com.mintris.ui.ProgressionScreen import java.text.SimpleDateFormat import java.util.* +import android.graphics.Color +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts class MainActivity : AppCompatActivity() { @@ -36,6 +44,8 @@ class MainActivity : AppCompatActivity() { 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 // Game state private var isSoundEnabled = true @@ -46,8 +56,19 @@ class MainActivity : AppCompatActivity() { 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 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) @@ -60,11 +81,30 @@ class MainActivity : AppCompatActivity() { gameMusic = GameMusic(this) highScoreManager = HighScoreManager(this) statsManager = StatsManager(this) + progressionManager = PlayerProgressionManager(this) + + // Load and apply theme preference + currentTheme = loadThemePreference() + applyTheme(currentTheme) // Set up game view gameView.setGameBoard(gameBoard) gameView.setHaptics(gameHaptics) + // Set up progression screen + progressionScreen = binding.progressionScreen + progressionScreen.visibility = View.GONE + progressionScreen.onContinue = { + progressionScreen.visibility = View.GONE + binding.gameOverContainer.visibility = View.VISIBLE + } + + // Set up theme selector + val themeSelector = binding.themeSelector + themeSelector.onThemeSelected = { themeId -> + applyTheme(themeId) + } + // Set up title screen titleScreen.onStartGame = { titleScreen.visibility = View.GONE @@ -244,6 +284,19 @@ class MainActivity : AppCompatActivity() { 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() @@ -263,26 +316,57 @@ class MainActivity : AppCompatActivity() { binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples()) binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises()) - // Check if this is a high score - if (highScoreManager.isHighScore(score)) { - val intent = Intent(this, HighScoreEntryActivity::class.java).apply { - putExtra("score", score) - putExtra("level", currentLevel) - } - startActivity(intent) - } + // Flag to track if high score screen will be shown + var showingHighScore = false - binding.gameOverContainer.visibility = View.VISIBLE + // Show progression screen first with XP animation + binding.gameOverContainer.visibility = View.GONE + progressionScreen.visibility = View.VISIBLE + progressionScreen.applyTheme(currentTheme) + progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme) + + // 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() + } + } + } // 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 } /** @@ -292,6 +376,13 @@ class MainActivity : AppCompatActivity() { 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)) + + // Update theme selector + updateThemeSelector() } /** @@ -346,6 +437,8 @@ class MainActivity : AppCompatActivity() { gameStartTime = System.currentTimeMillis() piecesPlaced = 0 statsManager.startNewSession() + progressionManager.startNewSession() + gameBoard.updateLevel(selectedLevel) } private fun restartGame() { @@ -378,6 +471,9 @@ class MainActivity : AppCompatActivity() { 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() { @@ -395,6 +491,7 @@ class MainActivity : AppCompatActivity() { binding.gameOverContainer.visibility = View.GONE binding.pauseContainer.visibility = View.GONE titleScreen.visibility = View.VISIBLE + titleScreen.applyTheme(currentTheme) } /** @@ -404,4 +501,104 @@ class MainActivity : AppCompatActivity() { 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) { + // Only apply if the theme is unlocked + if (!progressionManager.isThemeUnlocked(themeId)) return + + // Save the selected theme + currentTheme = themeId + saveThemePreference(themeId) + + // Apply theme to title screen if it's visible + if (titleScreen.visibility == View.VISIBLE) { + titleScreen.applyTheme(themeId) + } + + // Apply theme colors based on theme ID + when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> { + // Default black theme + binding.root.setBackgroundColor(Color.BLACK) + } + PlayerProgressionManager.THEME_NEON -> { + // Neon theme with dark purple background + binding.root.setBackgroundColor(Color.parseColor("#0D0221")) + } + PlayerProgressionManager.THEME_MONOCHROME -> { + // Monochrome dark gray + binding.root.setBackgroundColor(Color.parseColor("#1A1A1A")) + } + PlayerProgressionManager.THEME_RETRO -> { + // Retro arcade theme + binding.root.setBackgroundColor(Color.parseColor("#3F2832")) + } + PlayerProgressionManager.THEME_MINIMALIST -> { + // Minimalist white theme + binding.root.setBackgroundColor(Color.WHITE) + + // Update text colors for visibility + binding.scoreText.setTextColor(Color.BLACK) + binding.currentLevelText.setTextColor(Color.BLACK) + binding.linesText.setTextColor(Color.BLACK) + binding.comboText.setTextColor(Color.BLACK) + } + PlayerProgressionManager.THEME_GALAXY -> { + // Galaxy dark blue theme + binding.root.setBackgroundColor(Color.parseColor("#0B0C10")) + } + } + + // Apply theme to progression screen if it's visible and initialized + if (::progressionScreen.isInitialized && progressionScreen.visibility == View.VISIBLE) { + progressionScreen.applyTheme(themeId) + } + + // Update the game view to apply theme + gameView.invalidate() + } + + /** + * Save the selected theme in preferences + */ + private fun saveThemePreference(themeId: String) { + val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE) + prefs.edit().putString("selected_theme", themeId).apply() + } + + /** + * Load the saved theme preference + */ + private fun loadThemePreference(): String { + val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE) + return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC + } + + /** + * 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/StatsActivity.kt b/app/src/main/java/com/mintris/StatsActivity.kt index 269adb0..edcc765 100644 --- a/app/src/main/java/com/mintris/StatsActivity.kt +++ b/app/src/main/java/com/mintris/StatsActivity.kt @@ -3,6 +3,7 @@ package com.mintris import android.os.Bundle import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.mintris.databinding.ActivityStatsBinding import com.mintris.model.StatsManager @@ -25,9 +26,26 @@ class StatsActivity : AppCompatActivity() { finish() } + // Set up reset stats button + binding.resetStatsButton.setOnClickListener { + showResetConfirmationDialog() + } + updateStats() } + private fun showResetConfirmationDialog() { + AlertDialog.Builder(this) + .setTitle("Reset Stats") + .setMessage("Are you sure you want to reset all your stats? This cannot be undone.") + .setPositiveButton("Reset") { _, _ -> + statsManager.resetStats() + updateStats() + } + .setNegativeButton("Cancel", null) + .show() + } + private fun updateStats() { // Format time duration val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) diff --git a/app/src/main/java/com/mintris/audio/GameMusic.kt b/app/src/main/java/com/mintris/audio/GameMusic.kt index b455666..fbb8abf 100644 --- a/app/src/main/java/com/mintris/audio/GameMusic.kt +++ b/app/src/main/java/com/mintris/audio/GameMusic.kt @@ -9,10 +9,15 @@ import com.mintris.R class GameMusic(private val context: Context) { private var mediaPlayer: MediaPlayer? = null - private var isEnabled = false + private var isEnabled = true + private var isPrepared = false init { - setupMediaPlayer() + try { + setupMediaPlayer() + } catch (e: Exception) { + Log.e("GameMusic", "Error initializing: ${e.message}") + } } private fun setupMediaPlayer() { @@ -31,42 +36,47 @@ class GameMusic(private val context: Context) { .build() ) } + isPrepared = true } Log.d("GameMusic", "MediaPlayer setup complete") } catch (e: Exception) { Log.e("GameMusic", "Error setting up MediaPlayer", e) + mediaPlayer = null + isPrepared = false } } fun start() { - try { - Log.d("GameMusic", "Starting music playback, isEnabled: $isEnabled") - if (isEnabled && mediaPlayer?.isPlaying != true) { + if (isEnabled && mediaPlayer != null && isPrepared) { + try { + Log.d("GameMusic", "Starting music playback, isEnabled: $isEnabled") mediaPlayer?.start() Log.d("GameMusic", "Music playback started") + } catch (e: Exception) { + Log.e("GameMusic", "Error starting music: ${e.message}") } - } catch (e: Exception) { - Log.e("GameMusic", "Error starting music", e) } } fun pause() { try { Log.d("GameMusic", "Pausing music playback") - mediaPlayer?.pause() + if (mediaPlayer?.isPlaying == true) { + mediaPlayer?.pause() + } } catch (e: Exception) { - Log.e("GameMusic", "Error pausing music", e) + Log.e("GameMusic", "Error pausing music: ${e.message}") } } fun resume() { - try { - Log.d("GameMusic", "Resuming music playback") - if (isEnabled && mediaPlayer?.isPlaying != true) { + if (isEnabled && mediaPlayer != null && isPrepared) { + try { + Log.d("GameMusic", "Resuming music playback") mediaPlayer?.start() + } catch (e: Exception) { + Log.e("GameMusic", "Error resuming music: ${e.message}") } - } catch (e: Exception) { - Log.e("GameMusic", "Error resuming music", e) } } @@ -83,10 +93,11 @@ class GameMusic(private val context: Context) { fun setEnabled(enabled: Boolean) { Log.d("GameMusic", "Setting music enabled: $enabled") isEnabled = enabled - if (enabled) { - start() - } else { + + if (!enabled && mediaPlayer?.isPlaying == true) { pause() + } else if (enabled && mediaPlayer != null && isPrepared) { + start() } } @@ -97,8 +108,9 @@ class GameMusic(private val context: Context) { Log.d("GameMusic", "Releasing MediaPlayer") mediaPlayer?.release() mediaPlayer = null + isPrepared = false } catch (e: Exception) { - Log.e("GameMusic", "Error releasing MediaPlayer", e) + Log.e("GameMusic", "Error releasing music: ${e.message}") } } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 0c7d449..ce82647 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -133,6 +133,13 @@ class GameView @JvmOverloads constructor( private val minTapTime = 100L // Minimum time for a tap (in milliseconds) private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) + private var lockedDirection: Direction? = null // Track the locked movement direction + private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size + private val directionLockThreshold = 1.5f // Threshold for direction lock relative to block size + + private enum class Direction { + HORIZONTAL, VERTICAL + } // Callback for game events var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null @@ -578,6 +585,7 @@ class GameView @JvmOverloads constructor( startY = event.y lastTouchX = event.x lastTouchY = event.y + lockedDirection = null // Reset direction lock // Check for double tap (rotate) val currentTime = System.currentTimeMillis() @@ -597,32 +605,53 @@ class GameView @JvmOverloads constructor( val deltaY = event.y - lastTouchY val currentTime = System.currentTimeMillis() - // Horizontal movement (left/right) with reduced threshold - if (abs(deltaX) > blockSize * 0.5f) { // Reduced from 1.0f for more responsive movement - if (deltaX > 0) { - gameBoard.moveRight() - } else { - gameBoard.moveLeft() + // Determine movement direction if not locked + if (lockedDirection == null) { + val absDeltaX = abs(deltaX) + val absDeltaY = abs(deltaY) + + // Check if movement exceeds threshold + if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { + // Determine dominant direction + if (absDeltaX > absDeltaY * directionLockThreshold) { + lockedDirection = Direction.HORIZONTAL + } else if (absDeltaY > absDeltaX * directionLockThreshold) { + lockedDirection = Direction.VERTICAL + } } - lastTouchX = event.x - // Add haptic feedback for movement with cooldown - if (currentTime - lastMoveTime >= moveCooldown) { - gameHaptics?.vibrateForPieceMove() - lastMoveTime = currentTime - } - invalidate() } - // Vertical movement (soft drop) with reduced threshold - if (deltaY > blockSize * 0.25f) { // Reduced from 0.5f for more responsive soft drop - gameBoard.moveDown() - lastTouchY = event.y - // Add haptic feedback for movement with cooldown - if (currentTime - lastMoveTime >= moveCooldown) { - gameHaptics?.vibrateForPieceMove() - lastMoveTime = currentTime + // Handle movement based on locked direction + when (lockedDirection) { + Direction.HORIZONTAL -> { + if (abs(deltaX) > blockSize * minMovementThreshold) { + if (deltaX > 0) { + gameBoard.moveRight() + } else { + gameBoard.moveLeft() + } + lastTouchX = event.x + if (currentTime - lastMoveTime >= moveCooldown) { + gameHaptics?.vibrateForPieceMove() + lastMoveTime = currentTime + } + invalidate() + } + } + Direction.VERTICAL -> { + if (deltaY > blockSize * minMovementThreshold) { + gameBoard.moveDown() + lastTouchY = event.y + if (currentTime - lastMoveTime >= moveCooldown) { + gameHaptics?.vibrateForPieceMove() + lastMoveTime = currentTime + } + invalidate() + } + } + null -> { + // No direction lock yet, don't process movement } - invalidate() } } @@ -647,6 +676,9 @@ class GameView @JvmOverloads constructor( invalidate() } } + + // Reset direction lock + lockedDirection = null } } diff --git a/app/src/main/java/com/mintris/game/TitleScreen.kt b/app/src/main/java/com/mintris/game/TitleScreen.kt index 3a8978d..4c7a80f 100644 --- a/app/src/main/java/com/mintris/game/TitleScreen.kt +++ b/app/src/main/java/com/mintris/game/TitleScreen.kt @@ -12,6 +12,7 @@ import java.util.Random import android.util.Log import com.mintris.model.HighScoreManager import com.mintris.model.HighScore +import com.mintris.model.PlayerProgressionManager import kotlin.math.abs import androidx.core.graphics.withTranslation import androidx.core.graphics.withScale @@ -44,6 +45,9 @@ class TitleScreen @JvmOverloads constructor( // Callback for when the user touches the screen var onStartGame: (() -> Unit)? = null + + // Theme color + private var themeColor = Color.WHITE // Define tetromino shapes (I, O, T, S, Z, J, L) private val tetrominoShapes = arrayOf( @@ -116,7 +120,7 @@ class TitleScreen @JvmOverloads constructor( // "Touch to start" text settings promptPaint.apply { color = Color.WHITE - textSize = 40f + textSize = 50f textAlign = Paint.Align.CENTER typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL) isAntiAlias = true @@ -126,9 +130,9 @@ class TitleScreen @JvmOverloads constructor( // High scores text settings highScorePaint.apply { color = Color.WHITE - textSize = 35f - textAlign = Paint.Align.CENTER - typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL) + textSize = 70f + textAlign = Paint.Align.LEFT // Changed to LEFT alignment + typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace isAntiAlias = true alpha = 200 } @@ -241,9 +245,17 @@ class TitleScreen @JvmOverloads constructor( val highScores: List = highScoreManager.getHighScores() val highScoreY = height * 0.5f if (highScores.isNotEmpty()) { + // Calculate the starting X position to center the entire block of scores + val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999") + val startX = (width - maxScoreWidth) / 2 + highScores.forEachIndexed { index: Int, score: HighScore -> - val y = highScoreY + (index * 35f) - canvas.drawText("${index + 1}. ${score.name}: ${score.score}", width / 2f, y, highScorePaint) + val y = highScoreY + (index * 80f) + // Pad the rank number to ensure alignment + val rank = (index + 1).toString().padStart(2, ' ') + // Pad the name to ensure score alignment + val paddedName = score.name.padEnd(8, ' ') + canvas.drawText("$rank. $paddedName ${score.score}", startX, y, highScorePaint) } } @@ -304,4 +316,40 @@ class TitleScreen @JvmOverloads constructor( onStartGame?.invoke() return true } + + /** + * Apply a theme to the title screen + */ + fun applyTheme(themeId: String) { + // Get theme color based on theme ID + 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 + } + + // Update paint colors + titlePaint.color = themeColor + promptPaint.color = themeColor + highScorePaint.color = themeColor + paint.color = themeColor + glowPaint.color = themeColor + + // Update background color + setBackgroundColor(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 + }) + + invalidate() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index 42deaf3..5203109 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -29,6 +29,7 @@ class GameBoard( // Game state var score = 0 var level = 1 + var startingLevel = 1 // Add this line to track the starting level var lines = 0 var isGameOver = false var isHardDropInProgress = false // Make public @@ -70,7 +71,7 @@ class GameBoard( } // Take the next piece from the bag - nextPiece = Tetromino(bag.removeFirst()) + nextPiece = Tetromino(bag.removeAt(0)) onNextPieceChanged?.invoke() } @@ -439,7 +440,8 @@ class GameBoard( // Update lines cleared and level lines += clearedLines - level = (lines / 10) + 1 + // Calculate level based on lines cleared, but ensure it's never below the starting level + level = Math.max((lines / 10) + 1, startingLevel) // Update game speed based on level (NES formula) dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() @@ -519,6 +521,7 @@ class GameBoard( */ fun updateLevel(newLevel: Int) { level = newLevel.coerceIn(1, 20) + startingLevel = level // Store the starting level // Update game speed based on level (NES formula) dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() } @@ -546,10 +549,10 @@ class GameBoard( // Reset game state score = 0 - level = 1 + level = startingLevel // Use starting level instead of resetting to 1 lines = 0 isGameOver = false - dropInterval = 1000L // Reset to level 1 speed + dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() // Set speed based on current level // Reset scoring state combo = 0 diff --git a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt new file mode 100644 index 0000000..d25e9fd --- /dev/null +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -0,0 +1,345 @@ +package com.mintris.model + +import android.content.Context +import android.content.SharedPreferences +import com.mintris.R +import kotlin.math.pow +import kotlin.math.roundToInt + +/** + * Manages player progression, experience points, and unlockable rewards + */ +class PlayerProgressionManager(context: Context) { + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + // Player level and XP + private var playerLevel: Int = 1 + private var playerXP: Long = 0 + private var totalXPEarned: Long = 0 + + // Track unlocked rewards + private val unlockedThemes = mutableSetOf() + private val unlockedBlocks = mutableSetOf() + private val unlockedPowers = mutableSetOf() + private val unlockedBadges = mutableSetOf() + + // XP gained in the current session + private var sessionXPGained: Long = 0 + + init { + loadProgress() + } + + /** + * Load player progression data from shared preferences + */ + private fun loadProgress() { + playerLevel = prefs.getInt(KEY_PLAYER_LEVEL, 1) + playerXP = prefs.getLong(KEY_PLAYER_XP, 0) + totalXPEarned = prefs.getLong(KEY_TOTAL_XP_EARNED, 0) + + // Load unlocked rewards + val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf() + val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, setOf()) ?: setOf() + val powersSet = prefs.getStringSet(KEY_UNLOCKED_POWERS, setOf()) ?: setOf() + val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf() + + unlockedThemes.addAll(themesSet) + unlockedBlocks.addAll(blocksSet) + unlockedPowers.addAll(powersSet) + unlockedBadges.addAll(badgesSet) + + // Add default theme if nothing is unlocked + if (unlockedThemes.isEmpty()) { + unlockedThemes.add(THEME_CLASSIC) + } + } + + /** + * Save player progression data to shared preferences + */ + private fun saveProgress() { + prefs.edit() + .putInt(KEY_PLAYER_LEVEL, playerLevel) + .putLong(KEY_PLAYER_XP, playerXP) + .putLong(KEY_TOTAL_XP_EARNED, totalXPEarned) + .putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes) + .putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks) + .putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers) + .putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges) + .apply() + } + + /** + * Calculate XP required to reach a specific level + */ + fun calculateXPForLevel(level: Int): Long { + return (BASE_XP * level.toDouble().pow(XP_CURVE_FACTOR)).roundToInt().toLong() + } + + /** + * Calculate total XP required to reach a certain level from level 1 + */ + fun calculateTotalXPForLevel(level: Int): Long { + var totalXP = 0L + for (lvl in 1 until level) { + totalXP += calculateXPForLevel(lvl) + } + return totalXP + } + + /** + * Calculate XP from a game session based on score, lines, level, etc. + */ + fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long, + tetrisCount: Int, perfectClearCount: Int): Long { + // Base XP from score with level multiplier + val scoreXP = (score * (1 + LEVEL_MULTIPLIER * level)).toLong() + + // XP from lines cleared + val linesXP = lines * XP_PER_LINE + + // XP from special moves + val tetrisBonus = tetrisCount * TETRIS_XP_BONUS + val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS + + // Time bonus (to reward longer gameplay) + val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE // XP per minute played + + // Calculate total XP + return scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus + } + + /** + * Add XP to the player and handle level-ups + * Returns a list of newly unlocked rewards + */ + fun addXP(xpAmount: Long): List { + sessionXPGained = xpAmount + playerXP += xpAmount + totalXPEarned += xpAmount + + val newRewards = mutableListOf() + val oldLevel = playerLevel + + // Check for level ups + var xpForNextLevel = calculateXPForLevel(playerLevel) + while (playerXP >= xpForNextLevel) { + playerXP -= xpForNextLevel + playerLevel++ + + // Check for new rewards at this level + val levelRewards = checkLevelRewards(playerLevel) + newRewards.addAll(levelRewards) + + // Calculate XP needed for the next level + xpForNextLevel = calculateXPForLevel(playerLevel) + } + + // Save progress if there were any changes + if (oldLevel != playerLevel || newRewards.isNotEmpty()) { + saveProgress() + } + + return newRewards + } + + /** + * Check if the player unlocked new rewards at the current level + */ + private fun checkLevelRewards(level: Int): List { + val newRewards = mutableListOf() + + // Check for theme unlocks + when (level) { + 5 -> { + if (unlockedThemes.add(THEME_NEON)) { + newRewards.add("Unlocked Neon Theme!") + } + } + 10 -> { + if (unlockedThemes.add(THEME_MONOCHROME)) { + newRewards.add("Unlocked Monochrome Theme!") + } + } + 15 -> { + if (unlockedThemes.add(THEME_RETRO)) { + newRewards.add("Unlocked Retro Arcade Theme!") + } + } + 20 -> { + if (unlockedThemes.add(THEME_MINIMALIST)) { + newRewards.add("Unlocked Minimalist Theme!") + } + } + 25 -> { + if (unlockedThemes.add(THEME_GALAXY)) { + newRewards.add("Unlocked Galaxy Theme!") + } + } + } + + // Check for power unlocks + when (level) { + 8 -> { + if (unlockedPowers.add(POWER_FREEZE_TIME)) { + newRewards.add("Unlocked Freeze Time Power!") + } + } + 12 -> { + if (unlockedPowers.add(POWER_BLOCK_SWAP)) { + newRewards.add("Unlocked Block Swap Power!") + } + } + 18 -> { + if (unlockedPowers.add(POWER_SAFE_LANDING)) { + newRewards.add("Unlocked Safe Landing Power!") + } + } + 30 -> { + if (unlockedPowers.add(POWER_PERFECT_CLEAR)) { + newRewards.add("Unlocked Perfect Clear Power!") + } + } + } + + // Check for block skin unlocks + if (level % 7 == 0 && level <= 35) { + val blockSkin = "block_skin_${level / 7}" + if (unlockedBlocks.add(blockSkin)) { + newRewards.add("Unlocked New Block Skin!") + } + } + + return newRewards + } + + /** + * Start a new progression session + */ + fun startNewSession() { + sessionXPGained = 0 + } + + // Getters + fun getPlayerLevel(): Int = playerLevel + fun getCurrentXP(): Long = playerXP + fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel) + fun getSessionXPGained(): Long = sessionXPGained + fun getUnlockedThemes(): Set = unlockedThemes.toSet() + fun getUnlockedPowers(): Set = unlockedPowers.toSet() + fun getUnlockedBlocks(): Set = unlockedBlocks.toSet() + fun getUnlockedBadges(): Set = unlockedBadges.toSet() + + /** + * Check if a specific theme is unlocked + */ + fun isThemeUnlocked(themeId: String): Boolean { + return unlockedThemes.contains(themeId) + } + + /** + * Check if a specific power is unlocked + */ + fun isPowerUnlocked(powerId: String): Boolean { + return unlockedPowers.contains(powerId) + } + + /** + * Award a badge to the player + */ + fun awardBadge(badgeId: String): Boolean { + val newlyAwarded = unlockedBadges.add(badgeId) + if (newlyAwarded) { + saveProgress() + } + return newlyAwarded + } + + /** + * Reset all player progression data + */ + fun resetProgress() { + playerLevel = 1 + playerXP = 0 + totalXPEarned = 0 + + unlockedThemes.clear() + unlockedBlocks.clear() + unlockedPowers.clear() + unlockedBadges.clear() + + // Add default theme + unlockedThemes.add(THEME_CLASSIC) + + saveProgress() + } + + companion object { + private const val PREFS_NAME = "mintris_progression" + private const val KEY_PLAYER_LEVEL = "player_level" + private const val KEY_PLAYER_XP = "player_xp" + private const val KEY_TOTAL_XP_EARNED = "total_xp_earned" + private const val KEY_UNLOCKED_THEMES = "unlocked_themes" + private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks" + private const val KEY_UNLOCKED_POWERS = "unlocked_powers" + private const val KEY_UNLOCKED_BADGES = "unlocked_badges" + + // XP curve parameters + private const val BASE_XP = 5000.0 // Base XP for level 1 (increased from 2500) + private const val XP_CURVE_FACTOR = 2.2 // Exponential factor for XP curve (increased from 2.0) + + // XP calculation constants + private const val LEVEL_MULTIPLIER = 0.1 // 10% bonus per level + private const val XP_PER_LINE = 10L + private const val TETRIS_XP_BONUS = 50L + private const val PERFECT_CLEAR_XP_BONUS = 200L + private const val TIME_XP_PER_MINUTE = 5L + + // Theme IDs with required levels + const val THEME_CLASSIC = "theme_classic" + const val THEME_NEON = "theme_neon" + const val THEME_MONOCHROME = "theme_monochrome" + const val THEME_RETRO = "theme_retro" + const val THEME_MINIMALIST = "theme_minimalist" + const val THEME_GALAXY = "theme_galaxy" + + // Map of themes to required levels + val THEME_REQUIRED_LEVELS = mapOf( + THEME_CLASSIC to 1, + THEME_NEON to 5, + THEME_MONOCHROME to 10, + THEME_RETRO to 15, + THEME_MINIMALIST to 20, + THEME_GALAXY to 25 + ) + + // Power IDs + const val POWER_FREEZE_TIME = "power_freeze_time" + const val POWER_BLOCK_SWAP = "power_block_swap" + const val POWER_SAFE_LANDING = "power_safe_landing" + const val POWER_PERFECT_CLEAR = "power_perfect_clear" + + // Map of powers to required levels + val POWER_REQUIRED_LEVELS = mapOf( + POWER_FREEZE_TIME to 8, + POWER_BLOCK_SWAP to 12, + POWER_SAFE_LANDING to 18, + POWER_PERFECT_CLEAR to 30 + ) + } + + /** + * Get the required level for a specific theme + */ + fun getRequiredLevelForTheme(themeId: String): Int { + return THEME_REQUIRED_LEVELS[themeId] ?: 1 + } + + /** + * Get the required level for a specific power + */ + fun getRequiredLevelForPower(powerId: String): Int { + return POWER_REQUIRED_LEVELS[powerId] ?: 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/StatsManager.kt b/app/src/main/java/com/mintris/model/StatsManager.kt index 3a3eae7..cc5272f 100644 --- a/app/src/main/java/com/mintris/model/StatsManager.kt +++ b/app/src/main/java/com/mintris/model/StatsManager.kt @@ -157,6 +157,27 @@ class StatsManager(context: Context) { fun getSessionTriples(): Int = sessionTriples fun getSessionTetrises(): Int = sessionTetrises + fun resetStats() { + // Reset all lifetime stats + totalGames = 0 + totalScore = 0 + totalLines = 0 + totalPieces = 0 + totalTime = 0 + maxLevel = 0 + maxScore = 0 + maxLines = 0 + + // Reset line clear stats + totalSingles = 0 + totalDoubles = 0 + totalTriples = 0 + totalTetrises = 0 + + // Save the reset stats + saveStats() + } + companion object { private const val PREFS_NAME = "mintris_stats" private const val KEY_TOTAL_GAMES = "total_games" diff --git a/app/src/main/java/com/mintris/ui/LevelBadge.kt b/app/src/main/java/com/mintris/ui/LevelBadge.kt new file mode 100644 index 0000000..67665e4 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/LevelBadge.kt @@ -0,0 +1,67 @@ +package com.mintris.ui + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View + +/** + * Custom view for displaying the player's level in a fancy badge style + */ +class LevelBadge @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val badgePaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + } + + private val textPaint = Paint().apply { + color = Color.BLACK + isAntiAlias = true + textAlign = Paint.Align.CENTER + textSize = 48f + isFakeBoldText = true + } + + private var level = 1 + private var themeColor = Color.WHITE + + fun setLevel(newLevel: Int) { + level = newLevel + invalidate() + } + + fun setThemeColor(color: Int) { + themeColor = color + badgePaint.color = color + invalidate() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + // Adjust text size based on view size + textPaint.textSize = h * 0.6f + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw badge circle + val radius = (width.coerceAtMost(height) / 2f) * 0.9f + canvas.drawCircle(width / 2f, height / 2f, radius, badgePaint) + + // Draw level text + canvas.drawText( + level.toString(), + width / 2f, + height / 2f + (textPaint.textSize / 3), + textPaint + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ui/ProgressionScreen.kt b/app/src/main/java/com/mintris/ui/ProgressionScreen.kt new file mode 100644 index 0000000..bc67809 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/ProgressionScreen.kt @@ -0,0 +1,297 @@ +package com.mintris.ui + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.OvershootInterpolator +import android.widget.LinearLayout +import android.widget.TextView +import androidx.cardview.widget.CardView +import com.mintris.R +import com.mintris.model.PlayerProgressionManager + +/** + * Screen that displays player progression, XP gain, and unlocked rewards + */ +class ProgressionScreen @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + // UI components + private val xpProgressBar: XPProgressBar + private val xpGainText: TextView + private val playerLevelText: TextView + private val rewardsContainer: LinearLayout + private val continueButton: TextView + + // Callback for when the player dismisses the screen + var onContinue: (() -> Unit)? = null + + init { + orientation = VERTICAL + + // Inflate the layout + LayoutInflater.from(context).inflate(R.layout.progression_screen, this, true) + + // Get references to views + xpProgressBar = findViewById(R.id.xp_progress_bar) + xpGainText = findViewById(R.id.xp_gain_text) + playerLevelText = findViewById(R.id.player_level_text) + rewardsContainer = findViewById(R.id.rewards_container) + continueButton = findViewById(R.id.continue_button) + + // Set up button click listener + continueButton.setOnClickListener { + onContinue?.invoke() + } + } + + /** + * Display progression data and animate XP gain + */ + fun showProgress( + progressionManager: PlayerProgressionManager, + xpGained: Long, + newRewards: List, + themeId: String = PlayerProgressionManager.THEME_CLASSIC + ) { + // Hide rewards container initially if there are no new rewards + rewardsContainer.visibility = if (newRewards.isEmpty()) View.GONE else View.INVISIBLE + + // Set initial progress bar state + val playerLevel = progressionManager.getPlayerLevel() + val currentXP = progressionManager.getCurrentXP() + val xpForNextLevel = progressionManager.getXPForNextLevel() + + // Update texts + playerLevelText.text = "Player Level: $playerLevel" + xpGainText.text = "+$xpGained XP" + + // Begin animation sequence + xpProgressBar.setXPValues(playerLevel, currentXP, xpForNextLevel) + + // Animate XP gain text entrance + val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply { + duration = 500 + } + + // Schedule animation for the XP bar after text appears + postDelayed({ + xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel) + }, 600) + + // If there are new rewards, show them with animation + if (newRewards.isNotEmpty()) { + // Create reward cards + rewardsContainer.removeAllViews() + newRewards.forEach { reward -> + val rewardCard = createRewardCard(reward) + rewardsContainer.addView(rewardCard) + } + + // Apply theme to newly created reward cards + updateRewardCardColors(themeId) + + // Show rewards with animation after XP bar animation + postDelayed({ + rewardsContainer.visibility = View.VISIBLE + + // Animate each reward card + for (i in 0 until rewardsContainer.childCount) { + val card = rewardsContainer.getChildAt(i) + card.alpha = 0f + card.translationY = 100f + + // Stagger animation for each card + card.animate() + .alpha(1f) + .translationY(0f) + .setDuration(400) + .setStartDelay((i * 150).toLong()) + .setInterpolator(OvershootInterpolator()) + .start() + } + }, 2000) // Wait for XP bar animation to finish + } + + // Start with initial animations + AnimatorSet().apply { + play(xpTextAnimator) + start() + } + } + + /** + * Create a card view to display a reward + */ + private fun createRewardCard(rewardText: String): CardView { + val card = CardView(context).apply { + radius = 8f + cardElevation = 4f + useCompatPadding = true + + // Default background color - will be adjusted based on theme + setCardBackgroundColor(Color.BLACK) + + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(16, 8, 16, 8) + } + } + + // Add reward text + val textView = TextView(context).apply { + text = rewardText + setTextColor(Color.WHITE) + textSize = 18f + setPadding(16, 16, 16, 16) + textAlignment = View.TEXT_ALIGNMENT_CENTER + // Add some visual styling + typeface = android.graphics.Typeface.DEFAULT_BOLD + } + + card.addView(textView) + return card + } + + /** + * Apply the current theme to the progression screen + */ + fun applyTheme(themeId: String) { + // Get reference to the title text + val progressionTitle = findViewById(R.id.progression_title) + val rewardsTitle = findViewById(R.id.rewards_title) + + // Theme color for XP progress bar level badge + val xpThemeColor: Int + + // Apply theme colors based on theme ID + when (themeId) { + PlayerProgressionManager.THEME_CLASSIC -> { + // Default black theme + setBackgroundColor(Color.BLACK) + progressionTitle.setTextColor(Color.WHITE) + playerLevelText.setTextColor(Color.WHITE) + xpGainText.setTextColor(Color.WHITE) + continueButton.setTextColor(Color.WHITE) + rewardsTitle.setTextColor(Color.WHITE) + xpThemeColor = Color.WHITE + } + PlayerProgressionManager.THEME_NEON -> { + // Neon theme with dark purple background + setBackgroundColor(Color.parseColor("#0D0221")) + progressionTitle.setTextColor(Color.parseColor("#FF00FF")) + playerLevelText.setTextColor(Color.parseColor("#FF00FF")) + xpGainText.setTextColor(Color.WHITE) + continueButton.setTextColor(Color.parseColor("#FF00FF")) + rewardsTitle.setTextColor(Color.WHITE) + xpThemeColor = Color.parseColor("#FF00FF") + } + PlayerProgressionManager.THEME_MONOCHROME -> { + // Monochrome dark gray + setBackgroundColor(Color.parseColor("#1A1A1A")) + progressionTitle.setTextColor(Color.LTGRAY) + playerLevelText.setTextColor(Color.LTGRAY) + xpGainText.setTextColor(Color.WHITE) + continueButton.setTextColor(Color.LTGRAY) + rewardsTitle.setTextColor(Color.WHITE) + xpThemeColor = Color.LTGRAY + } + PlayerProgressionManager.THEME_RETRO -> { + // Retro arcade theme + setBackgroundColor(Color.parseColor("#3F2832")) + progressionTitle.setTextColor(Color.parseColor("#FF5A5F")) + playerLevelText.setTextColor(Color.parseColor("#FF5A5F")) + xpGainText.setTextColor(Color.WHITE) + continueButton.setTextColor(Color.parseColor("#FF5A5F")) + rewardsTitle.setTextColor(Color.WHITE) + xpThemeColor = Color.parseColor("#FF5A5F") + } + PlayerProgressionManager.THEME_MINIMALIST -> { + // Minimalist white theme + setBackgroundColor(Color.WHITE) + progressionTitle.setTextColor(Color.BLACK) + playerLevelText.setTextColor(Color.BLACK) + xpGainText.setTextColor(Color.BLACK) + continueButton.setTextColor(Color.BLACK) + rewardsTitle.setTextColor(Color.BLACK) + xpThemeColor = Color.BLACK + } + PlayerProgressionManager.THEME_GALAXY -> { + // Galaxy dark blue theme + setBackgroundColor(Color.parseColor("#0B0C10")) + progressionTitle.setTextColor(Color.parseColor("#66FCF1")) + playerLevelText.setTextColor(Color.parseColor("#66FCF1")) + xpGainText.setTextColor(Color.WHITE) + continueButton.setTextColor(Color.parseColor("#66FCF1")) + rewardsTitle.setTextColor(Color.WHITE) + xpThemeColor = Color.parseColor("#66FCF1") + } + else -> { + // Default fallback + setBackgroundColor(Color.BLACK) + progressionTitle.setTextColor(Color.WHITE) + playerLevelText.setTextColor(Color.WHITE) + xpGainText.setTextColor(Color.WHITE) + continueButton.setTextColor(Color.WHITE) + rewardsTitle.setTextColor(Color.WHITE) + xpThemeColor = Color.WHITE + } + } + + // Set theme color on XP progress bar + xpProgressBar.setThemeColor(xpThemeColor) + + // Update card colors for any existing reward cards + updateRewardCardColors(themeId) + } + + /** + * Update colors of existing reward cards to match the theme + */ + private fun updateRewardCardColors(themeId: String) { + // Color for card backgrounds based on theme + val cardBackgroundColor = 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 + } + + // Text color for rewards based on theme + val rewardTextColor = 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 + } + + // Update each card in the rewards container + for (i in 0 until rewardsContainer.childCount) { + val card = rewardsContainer.getChildAt(i) as? CardView + card?.let { + it.setCardBackgroundColor(cardBackgroundColor) + + // Update text color in the card + if (it.childCount > 0 && it.getChildAt(0) is TextView) { + (it.getChildAt(0) as TextView).setTextColor(rewardTextColor) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ui/ThemeSelector.kt b/app/src/main/java/com/mintris/ui/ThemeSelector.kt new file mode 100644 index 0000000..3be59c7 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/ThemeSelector.kt @@ -0,0 +1,273 @@ +package com.mintris.ui + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.GridLayout +import android.widget.TextView +import androidx.cardview.widget.CardView +import com.mintris.R +import com.mintris.model.PlayerProgressionManager + +/** + * UI component for selecting game themes + */ +class ThemeSelector @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val themesGrid: GridLayout + private val availableThemesLabel: TextView + + // Callback when a theme is selected + var onThemeSelected: ((String) -> Unit)? = null + + // Currently selected theme + private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC + + // Theme cards + private val themeCards = mutableMapOf() + + init { + // Inflate the layout + LayoutInflater.from(context).inflate(R.layout.theme_selector, this, true) + + // Get references to views + themesGrid = findViewById(R.id.themes_grid) + availableThemesLabel = findViewById(R.id.available_themes_label) + } + + /** + * Update the theme selector with unlocked themes + */ + fun updateThemes(unlockedThemes: Set, currentTheme: String) { + // Clear existing theme cards + themesGrid.removeAllViews() + themeCards.clear() + + // Update selected theme + selectedTheme = currentTheme + + // Get all possible themes and their details + val allThemes = getThemes() + + // Add theme cards to the grid + allThemes.forEach { (themeId, themeInfo) -> + val isUnlocked = unlockedThemes.contains(themeId) + val isSelected = themeId == selectedTheme + + val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected) + themeCards[themeId] = themeCard + themesGrid.addView(themeCard) + } + } + + /** + * Create a card for a theme + */ + private fun createThemeCard( + themeId: String, + themeInfo: ThemeInfo, + isUnlocked: Boolean, + isSelected: Boolean + ): CardView { + // Create the card + val card = CardView(context).apply { + id = View.generateViewId() + radius = 12f + cardElevation = if (isSelected) 8f else 2f + useCompatPadding = true + + // Set card background color based on theme + setCardBackgroundColor(themeInfo.primaryColor) + + // Add stroke for selected theme + if (isSelected) { + setContentPadding(4, 4, 4, 4) + // Create a gradient drawable for the border + val gradientDrawable = android.graphics.drawable.GradientDrawable().apply { + setColor(themeInfo.primaryColor) + setStroke(4, Color.WHITE) + cornerRadius = 12f + } + background = gradientDrawable + } + + // Set card dimensions + val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size) + layoutParams = GridLayout.LayoutParams().apply { + width = cardSize + height = cardSize + columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f) + rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f) + setMargins(8, 8, 8, 8) + } + + // Apply locked/selected state visuals + alpha = if (isUnlocked) 1.0f else 0.5f + } + + // Create theme content container + val container = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + } + + // Create the theme preview + val themePreview = View(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + + // Set the background color + setBackgroundColor(themeInfo.primaryColor) + } + + // Add a label with the theme name + val themeLabel = TextView(context).apply { + text = themeInfo.displayName + setTextColor(themeInfo.textColor) + textSize = 14f + textAlignment = View.TEXT_ALIGNMENT_CENTER + + // Position at the bottom of the card + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ).apply { + gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL + setMargins(4, 4, 4, 8) + } + } + + // Add level requirement for locked themes + val levelRequirement = TextView(context).apply { + text = "Level ${themeInfo.unlockLevel}" + setTextColor(Color.WHITE) + textSize = 12f + textAlignment = View.TEXT_ALIGNMENT_CENTER + visibility = if (isUnlocked) View.GONE else View.VISIBLE + + // Position at the center of the card + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ).apply { + gravity = android.view.Gravity.CENTER + } + // Make text bold and more visible for better readability + typeface = android.graphics.Typeface.DEFAULT_BOLD + setShadowLayer(3f, 1f, 1f, Color.BLACK) + } + + // Add a lock icon if the theme is locked + val lockOverlay = View(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + + // Add lock icon or visual indicator + setBackgroundResource(R.drawable.lock_overlay) + visibility = if (isUnlocked) View.GONE else View.VISIBLE + } + + // Add all elements to container + container.addView(themePreview) + container.addView(themeLabel) + container.addView(lockOverlay) + container.addView(levelRequirement) + + // Add container to card + card.addView(container) + + // Set up click listener only for unlocked themes + if (isUnlocked) { + card.setOnClickListener { + // Only trigger callback if this isn't already the selected theme + if (themeId != selectedTheme) { + // Update visual state + themeCards[selectedTheme]?.cardElevation = 2f + card.cardElevation = 8f + + // Update selected theme + selectedTheme = themeId + + // Notify listener + onThemeSelected?.invoke(themeId) + } + } + } + + return card + } + + /** + * Data class for theme information + */ + data class ThemeInfo( + val displayName: String, + val primaryColor: Int, + val secondaryColor: Int, + val textColor: Int, + val unlockLevel: Int + ) + + /** + * Get all available themes with their details + */ + private fun getThemes(): Map { + return mapOf( + PlayerProgressionManager.THEME_CLASSIC to ThemeInfo( + displayName = "Classic", + primaryColor = Color.parseColor("#000000"), + secondaryColor = Color.parseColor("#1F1F1F"), + textColor = Color.WHITE, + unlockLevel = 1 + ), + PlayerProgressionManager.THEME_NEON to ThemeInfo( + displayName = "Neon", + primaryColor = Color.parseColor("#0D0221"), + secondaryColor = Color.parseColor("#650D89"), + textColor = Color.parseColor("#FF00FF"), + unlockLevel = 5 + ), + PlayerProgressionManager.THEME_MONOCHROME to ThemeInfo( + displayName = "Monochrome", + primaryColor = Color.parseColor("#1A1A1A"), + secondaryColor = Color.parseColor("#333333"), + textColor = Color.LTGRAY, + unlockLevel = 10 + ), + PlayerProgressionManager.THEME_RETRO to ThemeInfo( + displayName = "Retro", + primaryColor = Color.parseColor("#3F2832"), + secondaryColor = Color.parseColor("#087E8B"), + textColor = Color.parseColor("#FF5A5F"), + unlockLevel = 15 + ), + PlayerProgressionManager.THEME_MINIMALIST to ThemeInfo( + displayName = "Minimalist", + primaryColor = Color.parseColor("#FFFFFF"), + secondaryColor = Color.parseColor("#F0F0F0"), + textColor = Color.BLACK, + unlockLevel = 20 + ), + PlayerProgressionManager.THEME_GALAXY to ThemeInfo( + displayName = "Galaxy", + primaryColor = Color.parseColor("#0B0C10"), + secondaryColor = Color.parseColor("#1F2833"), + textColor = Color.parseColor("#66FCF1"), + unlockLevel = 25 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ui/XPProgressBar.kt b/app/src/main/java/com/mintris/ui/XPProgressBar.kt new file mode 100644 index 0000000..c50ab6d --- /dev/null +++ b/app/src/main/java/com/mintris/ui/XPProgressBar.kt @@ -0,0 +1,243 @@ +package com.mintris.ui + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.core.content.ContextCompat +import com.mintris.R + +/** + * Custom progress bar for displaying player XP with animation capabilities + */ +class XPProgressBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + // Paints for drawing + private val backgroundPaint = Paint().apply { + color = Color.BLACK + isAntiAlias = true + } + + private val progressPaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + } + + private val textPaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + textAlign = Paint.Align.CENTER + textSize = 40f + } + + private val levelBadgePaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + } + + private val levelBadgeTextPaint = Paint().apply { + color = Color.BLACK + isAntiAlias = true + textAlign = Paint.Align.CENTER + textSize = 36f + isFakeBoldText = true + } + + // Progress bar dimensions + private val progressRect = RectF() + private val backgroundRect = RectF() + private val cornerRadius = 25f + + // Progress animation + private var progressAnimator: ValueAnimator? = null + private var currentProgress = 0f + private var targetProgress = 0f + + // XP values + private var currentXP = 0L + private var xpForNextLevel = 100L + private var playerLevel = 1 + + // Level up animation + private var isLevelingUp = false + private var levelUpAnimator: ValueAnimator? = null + private var levelBadgeScale = 1f + + // Theme-related properties + private var themeColor = Color.WHITE + + /** + * Set the player's current level and XP values + */ + fun setXPValues(level: Int, currentXP: Long, xpForNextLevel: Long) { + this.playerLevel = level + this.currentXP = currentXP + this.xpForNextLevel = xpForNextLevel + + // Update progress value + targetProgress = calculateProgressPercentage() + + // If not animating, set current progress immediately + if (progressAnimator == null || !progressAnimator!!.isRunning) { + currentProgress = targetProgress + } + + invalidate() + } + + /** + * Set theme color for elements + */ + fun setThemeColor(color: Int) { + themeColor = color + levelBadgePaint.color = color + invalidate() + } + + /** + * Animate adding XP to the bar + */ + fun animateXPGain(xpGained: Long, newLevel: Int, newCurrentXP: Long, newXPForNextLevel: Long) { + // Store original values before animation + val startXP = currentXP + val startLevel = playerLevel + + // Calculate percentage before XP gain + val startProgress = calculateProgressPercentage() + + // Update to new values + playerLevel = newLevel + currentXP = newCurrentXP + xpForNextLevel = newXPForNextLevel + + // Calculate new target progress + targetProgress = calculateProgressPercentage() + + // Determine if level up occurred + isLevelingUp = startLevel < newLevel + + // Animate progress bar + progressAnimator?.cancel() + progressAnimator = ValueAnimator.ofFloat(startProgress, targetProgress).apply { + duration = 1500 // 1.5 seconds animation + interpolator = AccelerateDecelerateInterpolator() + + addUpdateListener { animation -> + currentProgress = animation.animatedValue as Float + invalidate() + } + + // When animation completes, trigger level up animation if needed + if (isLevelingUp) { + levelUpAnimation() + } + + start() + } + } + + /** + * Create a level up animation effect + */ + private fun levelUpAnimation() { + levelUpAnimator?.cancel() + levelUpAnimator = ValueAnimator.ofFloat(1f, 1.5f, 1f).apply { + duration = 1000 // 1 second pulse animation + interpolator = AccelerateDecelerateInterpolator() + + addUpdateListener { animation -> + levelBadgeScale = animation.animatedValue as Float + invalidate() + } + + start() + } + } + + /** + * Calculate the current progress percentage + */ + private fun calculateProgressPercentage(): Float { + return if (xpForNextLevel > 0) { + (currentXP.toFloat() / xpForNextLevel.toFloat()) * 100f + } else { + 0f + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + // Update progress bar dimensions based on view size + val verticalPadding = h * 0.2f + // Increase left margin to prevent level badge from being cut off + backgroundRect.set( + h * 0.6f, // Increased from 0.5f to 0.6f for more space + verticalPadding, + w - paddingRight.toFloat(), + h - verticalPadding + ) + + // Adjust text size based on height + textPaint.textSize = h * 0.35f + levelBadgeTextPaint.textSize = h * 0.3f + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw level badge with adjusted position + val badgeRadius = height * 0.3f * levelBadgeScale + val badgeCenterX = height * 0.35f // Adjusted from 0.25f to 0.35f to match new position + val badgeCenterY = height * 0.5f + + canvas.drawCircle(badgeCenterX, badgeCenterY, badgeRadius, levelBadgePaint) + canvas.drawText( + playerLevel.toString(), + badgeCenterX, + badgeCenterY + (levelBadgeTextPaint.textSize / 3), + levelBadgeTextPaint + ) + + // Draw background bar + canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) + + // Draw progress bar + progressRect.set( + backgroundRect.left, + backgroundRect.top, + backgroundRect.left + (backgroundRect.width() * currentProgress / 100f), + backgroundRect.bottom + ) + + // Only draw if there is progress to show + if (progressRect.width() > 0) { + // Draw actual progress bar + canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint) + } + + // Draw progress text + val progressText = "${currentXP}/${xpForNextLevel} XP" + canvas.drawText( + progressText, + backgroundRect.centerX(), + backgroundRect.centerY() + (textPaint.textSize / 3), + textPaint + ) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + progressAnimator?.cancel() + levelUpAnimator?.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000..6e5f4fe --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/lock_overlay.xml b/app/src/main/res/drawable/lock_overlay.xml new file mode 100644 index 0000000..0bc4f2b --- /dev/null +++ b/app/src/main/res/drawable/lock_overlay.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_button.xml b/app/src/main/res/drawable/rounded_button.xml new file mode 100644 index 0000000..9553581 --- /dev/null +++ b/app/src/main/res/drawable/rounded_button.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ab63a51..e978405 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -234,6 +234,17 @@ android:textColor="@color/white" /> + + + - - -