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..c1ff90c --- /dev/null +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -0,0 +1,313 @@ +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 = 1000.0 // Base XP for level 1 + private const val XP_CURVE_FACTOR = 1.5 // Exponential factor for XP curve + + // 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 + 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" + + // 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" + } +} \ 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..3762b42 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/ProgressionScreen.kt @@ -0,0 +1,158 @@ +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.Button +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: Button + + // 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 + ) { + // 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) + } + + // 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 = 16f + cardElevation = 8f + useCompatPadding = true + setCardBackgroundColor(Color.parseColor("#FFD700")) // Gold background + + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(16, 16, 16, 16) + } + } + + // Add reward text + val textView = TextView(context).apply { + text = rewardText + setTextColor(Color.BLACK) + textSize = 18f + setPadding(32, 24, 32, 24) + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + + card.addView(textView) + return card + } +} \ 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..7f045a9 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/ThemeSelector.kt @@ -0,0 +1,234 @@ +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) + + // 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 + + // Add stroke for selected theme + if (isSelected) { + setContentPadding(4, 4, 4, 4) + } + } + + // 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 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 the card + card.addView(themePreview) + card.addView(themeLabel) + card.addView(lockOverlay) + + // 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..33d4546 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/XPProgressBar.kt @@ -0,0 +1,239 @@ +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.parseColor("#383838") + isAntiAlias = true + } + + private val progressPaint = Paint().apply { + color = Color.parseColor("#50C878") // Emerald green + isAntiAlias = true + } + + private val progressGlowPaint = Paint().apply { + color = Color.parseColor("#70F098") // Lighter emerald for glow + isAntiAlias = true + setShadowLayer(10f, 0f, 0f, Color.parseColor("#50C878")) + } + + private val textPaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + textAlign = Paint.Align.CENTER + textSize = 40f + } + + private val levelBadgePaint = Paint().apply { + color = Color.parseColor("#FFD700") // Gold color for level badge + 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 + + /** + * 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() + } + + /** + * 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 + backgroundRect.set( + h * 0.5f, // Left margin = height/2 (for level badge) + 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 + val badgeRadius = height * 0.3f * levelBadgeScale + val badgeCenterX = height * 0.25f + 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 glow effect first + canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressGlowPaint) + + // 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..4c016aa --- /dev/null +++ b/app/src/main/res/drawable/rounded_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ 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..0c9a1ef 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" /> + + + -