mintris/app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt

408 lines
No EOL
14 KiB
Kotlin

package com.pixelmintdrop.model
import android.content.Context
import android.content.SharedPreferences
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.min
import java.math.BigDecimal
import java.math.BigInteger
import kotlin.math.max
import com.pixelmintdrop.R // Assuming themes are defined elsewhere, maybe R?
/**
* 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<String>()
private val unlockedBlocks = mutableSetOf<String>()
private val unlockedBadges = mutableSetOf<String>()
// 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 {
// Use Double for calculations to maintain precision with multipliers
val scoreXP = score.toDouble() * SCORE_XP_MULTIPLIER
// Lines XP scales with level
val linesXP = lines.toDouble() * LINES_XP_MULTIPLIER * level.toDouble()
// Quad bonus scales inversely with level (harder quads at higher levels mean less relative bonus?) - capped at 0.5 minimum multiplier
val quadBonusMultiplier = (1.0 - (level - 1).toDouble() * 0.05).coerceAtLeast(0.5)
val quadBonus = quadCount.toDouble() * QUAD_XP_BONUS * quadBonusMultiplier
// Perfect clear bonus is a fixed Long value
val perfectClearBonusXP = perfectClearCount * PERFECT_CLEAR_XP_BONUS // This is already Long
// Time bonus based on seconds played
val timeBonus = (timePlayedMs / 1000.0) * TIME_XP_MULTIPLIER
// Summing up components (ensure consistent types, cast bonus to Double if needed)
val totalXP = scoreXP + linesXP + quadBonus + perfectClearBonusXP.toDouble() + timeBonus
// Return the final XP as Long
return max(0L, totalXP.toLong()) // Ensure XP is not negative and return Long
}
/**
* Add XP to the player and handle level-ups
* Returns a list of newly unlocked rewards
*/
fun addXP(xpAmount: Long): List<String> {
sessionXPGained = xpAmount
playerXP += xpAmount
totalXPEarned += xpAmount
val newRewards = mutableListOf<String>()
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 *new* next level
xpForNextLevel = calculateXPForLevel(playerLevel)
}
// Save progress if level changed or rewards were unlocked
if (oldLevel != playerLevel || newRewards.isNotEmpty() || xpAmount > 0) {
saveProgress()
}
return newRewards
}
/**
* Check if the player unlocked new rewards at the current level
*/
private fun checkLevelRewards(level: Int): List<String> {
val newRewards = mutableListOf<String>()
var newlyUnlocked = false // Track if anything was actually unlocked in this call
// Check for theme unlocks
when (level) {
5 -> {
if (unlockedThemes.add(THEME_NEON)) {
newRewards.add("Unlocked Neon Theme!")
newlyUnlocked = true
}
}
10 -> {
if (unlockedThemes.add(THEME_MONOCHROME)) {
newRewards.add("Unlocked Monochrome Theme!")
newlyUnlocked = true
}
}
15 -> {
if (unlockedThemes.add(THEME_RETRO)) {
newRewards.add("Unlocked Retro Arcade Theme!")
newlyUnlocked = true
}
}
20 -> {
if (unlockedThemes.add(THEME_MINIMALIST)) {
newRewards.add("Unlocked Minimalist Theme!")
newlyUnlocked = true
}
}
25 -> {
if (unlockedThemes.add(THEME_GALAXY)) {
newRewards.add("Unlocked Galaxy Theme!")
newlyUnlocked = true
}
}
}
// 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!")
newlyUnlocked = true
}
}
14 -> {
if (unlockedBlocks.add("block_skin_3")) {
newRewards.add("Unlocked Retro Block Skin!")
newlyUnlocked = true
}
}
21 -> {
if (unlockedBlocks.add("block_skin_4")) {
newRewards.add("Unlocked Minimalist Block Skin!")
newlyUnlocked = true
}
}
28 -> {
if (unlockedBlocks.add("block_skin_5")) {
newRewards.add("Unlocked Galaxy Block Skin!")
newlyUnlocked = true
}
}
}
// Save progress immediately if something was unlocked by this check
if (newlyUnlocked) {
saveProgress()
}
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 getTotalXPEarned(): Long = totalXPEarned // Added getter for total XP
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet() // Return immutable copy
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet() // Return immutable copy
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet() // Return immutable copy
/**
* 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 = "unlockedBadges"
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
)
// XP Calculation Constants (Added)
private const val BASE_XP_PER_LEVEL = 1000.0
private const val XP_LEVEL_MULTIPLIER = 1.15 // XP increases by 15% each level
// Game XP Constants (Added)
private const val SCORE_XP_MULTIPLIER = 0.05 // 1 XP per 20 points
private const val LINES_XP_MULTIPLIER = 25.0 // Base XP per line
private const val QUAD_XP_BONUS = 150.0 // Bonus XP per Quad
private const val TIME_XP_MULTIPLIER = 0.5 // 0.5 XP per second played
}
/**
* 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
}
/**
* Check if a specific block skin is unlocked
*/
fun isBlockSkinUnlocked(blockSkinId: String): Boolean {
return unlockedBlocks.contains(blockSkinId)
}
}