diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 95ef64c..5390803 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -19,6 +19,8 @@ import com.mintris.model.HighScoreManager import com.mintris.model.PlayerProgressionManager import com.mintris.model.StatsManager import com.mintris.ui.ProgressionScreen +import com.mintris.ui.ThemeSelector +import com.mintris.ui.BlockSkinSelector import java.text.SimpleDateFormat import java.util.* import android.graphics.Color @@ -45,6 +47,8 @@ class MainActivity : AppCompatActivity() { private lateinit var statsManager: StatsManager private lateinit var progressionManager: PlayerProgressionManager private lateinit var progressionScreen: ProgressionScreen + private lateinit var themeSelector: ThemeSelector + private lateinit var blockSkinSelector: BlockSkinSelector // Game state private var isSoundEnabled = true @@ -84,11 +88,16 @@ class MainActivity : AppCompatActivity() { highScoreManager = HighScoreManager(this) statsManager = StatsManager(this) progressionManager = PlayerProgressionManager(this) + themeSelector = binding.themeSelector + blockSkinSelector = binding.blockSkinSelector // Load and apply theme preference - currentTheme = loadThemePreference() + currentTheme = progressionManager.getSelectedTheme() applyTheme(currentTheme) + // Load and apply block skin preference + gameView.setBlockSkin(progressionManager.getSelectedBlockSkin()) + // Set up game view gameView.setGameBoard(gameBoard) gameView.setHaptics(gameHaptics) @@ -102,8 +111,7 @@ class MainActivity : AppCompatActivity() { } // Set up theme selector - val themeSelector = binding.themeSelector - themeSelector.onThemeSelected = { themeId -> + themeSelector.onThemeSelected = { themeId: String -> // Apply the new theme applyTheme(themeId) @@ -116,6 +124,18 @@ class MainActivity : AppCompatActivity() { } } + // Set up block skin selector + blockSkinSelector.onBlockSkinSelected = { skinId: String -> + // Apply the new block skin + gameView.setBlockSkin(skinId) + + // Save the selection + progressionManager.setSelectedBlockSkin(skinId) + + // Provide haptic feedback + gameHaptics.vibrateForPieceLock() + } + // Set up title screen titleScreen.onStartGame = { titleScreen.visibility = View.GONE @@ -426,6 +446,13 @@ class MainActivity : AppCompatActivity() { // Update theme selector updateThemeSelector() + + // Update block skin selector + blockSkinSelector.updateBlockSkins( + progressionManager.getUnlockedBlocks(), + gameView.getCurrentBlockSkin(), + progressionManager.getPlayerLevel() + ) } /** @@ -564,7 +591,7 @@ class MainActivity : AppCompatActivity() { // Save the selected theme currentTheme = themeId - saveThemePreference(themeId) + progressionManager.setSelectedTheme(themeId) // Apply theme to title screen if it's visible if (titleScreen.visibility == View.VISIBLE) { @@ -618,22 +645,6 @@ class MainActivity : AppCompatActivity() { 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 */ diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 7f323be..3b32723 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -144,6 +144,10 @@ class GameView @JvmOverloads constructor( private val isStrictDirectionLock = true // Enable strict direction locking to prevent diagonal inputs private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture + // Block skin + private var currentBlockSkin: String = "block_skin_1" + private val blockSkinPaints = mutableMapOf() + private enum class Direction { HORIZONTAL, VERTICAL } @@ -206,8 +210,64 @@ class GameView @JvmOverloads constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height))) } + + // Initialize block skin paints + initializeBlockSkinPaints() } + /** + * Initialize paints for different block skins + */ + private fun initializeBlockSkinPaints() { + // Classic skin + blockSkinPaints["block_skin_1"] = Paint().apply { + color = Color.WHITE + isAntiAlias = true + } + + // Neon skin + blockSkinPaints["block_skin_2"] = Paint().apply { + color = Color.parseColor("#FF00FF") + isAntiAlias = true + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) + } + + // Retro skin + blockSkinPaints["block_skin_3"] = Paint().apply { + color = Color.parseColor("#FF5A5F") + isAntiAlias = true + style = Paint.Style.STROKE + strokeWidth = 2f + } + + // Minimalist skin + blockSkinPaints["block_skin_4"] = Paint().apply { + color = Color.BLACK + isAntiAlias = true + style = Paint.Style.FILL + } + + // Galaxy skin + blockSkinPaints["block_skin_5"] = Paint().apply { + color = Color.parseColor("#66FCF1") + isAntiAlias = true + maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) + } + } + + /** + * Set the current block skin + */ + fun setBlockSkin(skinId: String) { + currentBlockSkin = skinId + invalidate() + } + + /** + * Get the current block skin + */ + fun getCurrentBlockSkin(): String = currentBlockSkin + /** * Start the game */ @@ -519,16 +579,19 @@ class GameView @JvmOverloads constructor( // Save canvas state before drawing block effects canvas.save() + // Get the current block skin paint + val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!! + // Draw outer glow blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint) - // Draw block - blockPaint.apply { - color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE + // Draw block with current skin + paint.apply { + color = if (isGhost) Color.argb(30, 255, 255, 255) else paint.color alpha = if (isGhost) 30 else 255 } - canvas.drawRect(left, top, right, bottom, blockPaint) + canvas.drawRect(left, top, right, bottom, paint) // Draw inner glow glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE diff --git a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt index e9260bd..bf62a6a 100644 --- a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -20,7 +20,6 @@ class PlayerProgressionManager(context: Context) { // 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 @@ -41,18 +40,21 @@ class PlayerProgressionManager(context: Context) { // 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) } + + // Add default block skin if nothing is unlocked + if (unlockedBlocks.isEmpty()) { + unlockedBlocks.add("block_skin_1") + } } /** @@ -65,7 +67,6 @@ class PlayerProgressionManager(context: Context) { .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() } @@ -179,30 +180,6 @@ class PlayerProgressionManager(context: Context) { } } - // 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}" @@ -214,11 +191,38 @@ class PlayerProgressionManager(context: Context) { 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 + for (i in 1..5) { + val requiredLevel = i * 7 + if (playerLevel >= requiredLevel) { + unlockedBlocks.add("block_skin_$i") + } + } + + // Save any newly unlocked items + saveProgress() + } + /** * Start a new progression session */ fun startNewSession() { sessionXPGained = 0 + + // Ensure all appropriate unlocks are available + checkAllUnlocksForCurrentLevel() } // Getters @@ -227,7 +231,6 @@ class PlayerProgressionManager(context: Context) { 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() @@ -238,13 +241,6 @@ class PlayerProgressionManager(context: Context) { 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 */ @@ -266,12 +262,14 @@ class PlayerProgressionManager(context: Context) { unlockedThemes.clear() unlockedBlocks.clear() - unlockedPowers.clear() unlockedBadges.clear() // Add default theme unlockedThemes.add(THEME_CLASSIC) + // Add default block skin + unlockedBlocks.add("block_skin_1") + saveProgress() } @@ -282,8 +280,9 @@ class PlayerProgressionManager(context: Context) { 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" + private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin" + private const val KEY_SELECTED_THEME = "selected_theme" // XP curve parameters private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000) @@ -313,20 +312,6 @@ class PlayerProgressionManager(context: Context) { 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 - ) } /** @@ -337,9 +322,34 @@ class PlayerProgressionManager(context: Context) { } /** - * Get the required level for a specific power + * Set the selected block skin */ - fun getRequiredLevelForPower(powerId: String): Int { - return POWER_REQUIRED_LEVELS[powerId] ?: 1 + fun setSelectedBlockSkin(skinId: String) { + if (unlockedBlocks.contains(skinId)) { + prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).apply() + } + } + + /** + * 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt b/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt new file mode 100644 index 0000000..15255a6 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt @@ -0,0 +1,288 @@ +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 block skins + */ +class BlockSkinSelector @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val skinsGrid: GridLayout + private val availableSkinsLabel: TextView + + // Callback when a block skin is selected + var onBlockSkinSelected: ((String) -> Unit)? = null + + // Currently selected block skin + private var selectedSkin: String = "block_skin_1" + + // Block skin cards + private val skinCards = mutableMapOf() + + // Player level for determining what should be unlocked + private var playerLevel: Int = 1 + + init { + // Inflate the layout + LayoutInflater.from(context).inflate(R.layout.block_skin_selector, this, true) + + // Get references to views + skinsGrid = findViewById(R.id.skins_grid) + availableSkinsLabel = findViewById(R.id.available_skins_label) + } + + /** + * Update the block skin selector with unlocked skins + */ + fun updateBlockSkins(unlockedSkins: Set, currentSkin: String, playerLevel: Int = 1) { + // Store player level + this.playerLevel = playerLevel + + // Clear existing skin cards + skinsGrid.removeAllViews() + skinCards.clear() + + // Update selected skin + selectedSkin = currentSkin + + // Get all possible skins and their details + val allSkins = getBlockSkins() + + // Add skin cards to the grid + allSkins.forEach { (skinId, skinInfo) -> + val isUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel + val isSelected = skinId == selectedSkin + + val skinCard = createBlockSkinCard(skinId, skinInfo, isUnlocked, isSelected) + skinCards[skinId] = skinCard + skinsGrid.addView(skinCard) + } + } + + /** + * Create a card for a block skin + */ + private fun createBlockSkinCard( + skinId: String, + skinInfo: BlockSkinInfo, + 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 skin + setCardBackgroundColor(skinInfo.backgroundColor) + + // Add more noticeable visual indicator for selected skin + if (isSelected) { + setContentPadding(4, 4, 4, 4) + // Create a gradient drawable for the border + val gradientDrawable = android.graphics.drawable.GradientDrawable().apply { + setColor(skinInfo.backgroundColor) + setStroke(6, Color.WHITE) // Thicker border + cornerRadius = 12f + } + background = gradientDrawable + // Add glow effect via elevation + cardElevation = 12f + } + + // 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 block skin content container + val container = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + } + + // Create the block skin preview + val blockSkinPreview = View(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + + // Set the background color + setBackgroundColor(skinInfo.backgroundColor) + } + + // Add a label with the skin name + val skinLabel = TextView(context).apply { + text = skinInfo.displayName + setTextColor(skinInfo.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 skins + val levelRequirement = TextView(context).apply { + text = "Level ${skinInfo.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 skin 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(blockSkinPreview) + container.addView(skinLabel) + container.addView(lockOverlay) + container.addView(levelRequirement) + + // Add container to card + card.addView(container) + + // Set up click listener only for unlocked skins + if (isUnlocked) { + card.setOnClickListener { + // Only trigger callback if this isn't already the selected skin + if (skinId != selectedSkin) { + // Update previously selected card + skinCards[selectedSkin]?.let { prevCard -> + prevCard.cardElevation = 2f + // Reset any special styling + prevCard.background = null + prevCard.setCardBackgroundColor(getBlockSkins()[selectedSkin]?.backgroundColor ?: Color.BLACK) + } + + // Update visual state of newly selected card + card.cardElevation = 12f + + // Flash animation for selection feedback + val flashColor = Color.WHITE + val originalColor = skinInfo.backgroundColor + + // Create animator for flash effect + val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor) + flashAnimator.duration = 300 // 300ms + flashAnimator.addUpdateListener { animator -> + val color = animator.animatedValue as Int + card.setCardBackgroundColor(color) + } + flashAnimator.start() + + // Update selected skin + selectedSkin = skinId + + // Notify listener + onBlockSkinSelected?.invoke(skinId) + } + } + } + + return card + } + + /** + * Data class for block skin information + */ + data class BlockSkinInfo( + val displayName: String, + val backgroundColor: Int, + val textColor: Int, + val unlockLevel: Int + ) + + /** + * Get all available block skins with their details + */ + private fun getBlockSkins(): Map { + return mapOf( + "block_skin_1" to BlockSkinInfo( + displayName = "Classic", + backgroundColor = Color.BLACK, + textColor = Color.WHITE, + unlockLevel = 1 + ), + "block_skin_2" to BlockSkinInfo( + displayName = "Neon", + backgroundColor = Color.parseColor("#0D0221"), + textColor = Color.parseColor("#FF00FF"), + unlockLevel = 7 + ), + "block_skin_3" to BlockSkinInfo( + displayName = "Retro", + backgroundColor = Color.parseColor("#3F2832"), + textColor = Color.parseColor("#FF5A5F"), + unlockLevel = 14 + ), + "block_skin_4" to BlockSkinInfo( + displayName = "Minimalist", + backgroundColor = Color.WHITE, + textColor = Color.BLACK, + unlockLevel = 21 + ), + "block_skin_5" to BlockSkinInfo( + displayName = "Galaxy", + backgroundColor = Color.parseColor("#0B0C10"), + textColor = Color.parseColor("#66FCF1"), + unlockLevel = 28 + ) + ) + } +} \ 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 939dc48..ddea78a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -401,6 +401,14 @@ android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginBottom="16dp" /> + + +