package com.pixelmintdrop.model import android.content.Context import android.content.SharedPreferences import kotlin.math.pow import kotlin.math.roundToInt import kotlin.math.min /** * 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 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 badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf() unlockedThemes.addAll(themesSet) unlockedBlocks.addAll(blocksSet) unlockedBadges.addAll(badgesSet) // Add default theme if nothing is unlocked if (unlockedThemes.isEmpty()) { unlockedThemes.add(THEME_CLASSIC) } // Always ensure default block skin is present (Level 1) unlockedBlocks.add("block_skin_1") // Explicitly check and save all unlocks for the current level on load checkAllUnlocksForCurrentLevel() } /** * 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_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, timePlayedMs: Long, quadCount: Int, perfectClearCount: Int): Long { val scoreXP = score * SCORE_XP_MULTIPLIER val linesXP = lines * LINES_XP_MULTIPLIER * level val quadBonus = quadCount * QUAD_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5) val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS val timeBonus = (timePlayedMs / 1000) * TIME_XP_MULTIPLIER return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong() } /** * 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 block skin unlocks (start from skin 2 at level 7) when (level) { 7 -> { if (unlockedBlocks.add("block_skin_2")) { newRewards.add("Unlocked Neon Block Skin!") } } 14 -> { if (unlockedBlocks.add("block_skin_3")) { newRewards.add("Unlocked Retro Block Skin!") } } 21 -> { if (unlockedBlocks.add("block_skin_4")) { newRewards.add("Unlocked Minimalist Block Skin!") } } 28 -> { if (unlockedBlocks.add("block_skin_5")) { newRewards.add("Unlocked Galaxy Block Skin!") } } } return newRewards } /** * Check and unlock any rewards the player should have based on their current level * This ensures players don't miss unlocks if they level up multiple times at once */ private fun checkAllUnlocksForCurrentLevel() { // Check theme unlocks if (playerLevel >= 5) unlockedThemes.add(THEME_NEON) if (playerLevel >= 10) unlockedThemes.add(THEME_MONOCHROME) if (playerLevel >= 15) unlockedThemes.add(THEME_RETRO) if (playerLevel >= 20) unlockedThemes.add(THEME_MINIMALIST) if (playerLevel >= 25) unlockedThemes.add(THEME_GALAXY) // Check block skin unlocks (start from skin 2 at level 7) // Skin 1 is default (added in loadProgress) if (playerLevel >= 7) unlockedBlocks.add("block_skin_2") if (playerLevel >= 14) unlockedBlocks.add("block_skin_3") if (playerLevel >= 21) unlockedBlocks.add("block_skin_4") if (playerLevel >= 28) unlockedBlocks.add("block_skin_5") // Save any newly unlocked items saveProgress() } /** * Start a new progression session */ fun startNewSession() { sessionXPGained = 0 // Ensure all appropriate unlocks are available checkAllUnlocksForCurrentLevel() } // Getters fun getPlayerLevel(): Int = playerLevel fun getCurrentXP(): Long = playerXP fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel) fun getSessionXPGained(): Long = sessionXPGained fun getUnlockedThemes(): Set = unlockedThemes.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) } /** * 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() unlockedBadges.clear() // Add default theme unlockedThemes.add(THEME_CLASSIC) // Add default block skin (Level 1) unlockedBlocks.add("block_skin_1") saveProgress() } companion object { private const val PREFS_NAME = "pixelmintdrop_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_BADGES = "unlocked_badges" private const val KEY_SELECTED_THEME = "selected_theme" private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin" // XP constants private const val BASE_XP = 3000L private const val XP_CURVE_FACTOR = 2.0 private const val LEVEL_MULTIPLIER = 0.03 private const val XP_PER_LINE = 40L private const val TETRIS_XP_BONUS = 150L private const val PERFECT_CLEAR_XP_BONUS = 300L private const val TIME_XP_PER_MINUTE = 20L // Theme constants 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 ) } /** * Get the required level for a specific theme */ fun getRequiredLevelForTheme(themeId: String): Int { return THEME_REQUIRED_LEVELS[themeId] ?: 1 } /** * Set the selected block skin */ fun setSelectedBlockSkin(skinId: String) { if (unlockedBlocks.contains(skinId)) { prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).commit() } } /** * Get the selected block skin */ fun getSelectedBlockSkin(): String { return prefs.getString(KEY_SELECTED_BLOCK_SKIN, "block_skin_1") ?: "block_skin_1" } /** * Set the selected theme */ fun setSelectedTheme(themeId: String) { if (unlockedThemes.contains(themeId)) { prefs.edit().putString(KEY_SELECTED_THEME, themeId).apply() } } /** * Get the selected theme */ fun getSelectedTheme(): String { return prefs.getString(KEY_SELECTED_THEME, THEME_CLASSIC) ?: THEME_CLASSIC } }