mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-19 06:25:19 +01:00
Fix: Correct namespace and applicationId in app build.gradle
This commit is contained in:
parent
5cf8aec02a
commit
5ace9d7fc5
25 changed files with 4 additions and 4 deletions
433
app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt
Normal file
433
app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt
Normal file
|
@ -0,0 +1,433 @@
|
|||
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
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
|
||||
/**
|
||||
* 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 (persisted)
|
||||
private var selectedSkin: String = "block_skin_1"
|
||||
|
||||
// Block skin cards map (skinId -> CardView)
|
||||
private val skinCards = mutableMapOf<String, CardView>()
|
||||
// Ordered list of skin IDs for navigation
|
||||
private val skinIdList = mutableListOf<String>()
|
||||
// Currently focused skin ID (for gamepad navigation within the selector)
|
||||
private var focusedSkinId: String? = null
|
||||
// Index of the currently focused skin in skinIdList
|
||||
private var focusedIndex: Int = -1
|
||||
// Flag indicating if the entire selector component has focus from the main menu
|
||||
private var hasComponentFocus: Boolean = false
|
||||
|
||||
// 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<String>, currentSkin: String, playerLevel: Int = 1) {
|
||||
// Store player level
|
||||
this.playerLevel = playerLevel
|
||||
|
||||
// Clear existing skin cards and ID list
|
||||
skinsGrid.removeAllViews()
|
||||
skinCards.clear()
|
||||
skinIdList.clear()
|
||||
|
||||
// Update selected skin and initial focus
|
||||
selectedSkin = currentSkin
|
||||
focusedSkinId = currentSkin
|
||||
focusedIndex = -1 // Reset index
|
||||
|
||||
// Get all possible skins and their details, sorted for consistent order
|
||||
val allSkins = getBlockSkins().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
||||
|
||||
// Add skin cards to the grid and build ID list
|
||||
allSkins.forEach { (skinId, skinInfo) ->
|
||||
val isEffectivelyUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
|
||||
val isSelected = skinId == selectedSkin
|
||||
|
||||
// Only add unlocked skins to the navigable list
|
||||
if (isEffectivelyUnlocked) {
|
||||
skinIdList.add(skinId)
|
||||
}
|
||||
|
||||
val skinCard = createBlockSkinCard(skinId, skinInfo, isEffectivelyUnlocked, isSelected)
|
||||
skinCards[skinId] = skinCard
|
||||
skinsGrid.addView(skinCard)
|
||||
|
||||
// Update focused index if this is the currently selected/focused skin
|
||||
if (isEffectivelyUnlocked && skinId == focusedSkinId) {
|
||||
focusedIndex = skinIdList.indexOf(skinId)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure focus index is valid if the previously focused skin is no longer available/unlocked
|
||||
if (focusedIndex == -1 && skinIdList.isNotEmpty()) {
|
||||
focusedIndex = 0
|
||||
focusedSkinId = skinIdList[0]
|
||||
}
|
||||
|
||||
// Apply initial focus highlight if the component has focus
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
// 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 (only opacity here)
|
||||
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 {
|
||||
// Clicking directly selects the skin
|
||||
focusedSkinId = skinId
|
||||
focusedIndex = skinIdList.indexOf(skinId)
|
||||
confirmSelection() // Directly confirm click selection
|
||||
}
|
||||
}
|
||||
|
||||
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<String, BlockSkinInfo> {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the entire component has focus (from the parent menu).
|
||||
*/
|
||||
fun setHasFocus(hasFocus: Boolean) {
|
||||
hasComponentFocus = hasFocus
|
||||
highlightFocusedCard() // Re-apply highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the next available skin.
|
||||
*/
|
||||
fun focusNextItem() {
|
||||
if (!hasComponentFocus || skinIdList.isEmpty()) return
|
||||
|
||||
focusedIndex = (focusedIndex + 1) % skinIdList.size
|
||||
focusedSkinId = skinIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the previous available skin.
|
||||
*/
|
||||
fun focusPreviousItem() {
|
||||
if (!hasComponentFocus || skinIdList.isEmpty()) return
|
||||
|
||||
focusedIndex = (focusedIndex - 1 + skinIdList.size) % skinIdList.size
|
||||
focusedSkinId = skinIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the currently focused skin as the selected skin.
|
||||
* Triggers the onBlockSkinSelected callback.
|
||||
*/
|
||||
fun confirmSelection() {
|
||||
if (focusedSkinId == null || focusedSkinId == selectedSkin) {
|
||||
return // No change needed
|
||||
}
|
||||
|
||||
// Update the selected skin
|
||||
val newlySelectedSkin = focusedSkinId!!
|
||||
selectedSkin = newlySelectedSkin
|
||||
|
||||
// Update visual states
|
||||
highlightFocusedCard()
|
||||
|
||||
// Trigger the callback
|
||||
onBlockSkinSelected?.invoke(selectedSkin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual highlight state of the skin cards based on
|
||||
* selection and internal focus.
|
||||
*/
|
||||
private fun highlightFocusedCard() {
|
||||
if (skinCards.isEmpty()) return
|
||||
|
||||
val focusColor = Color.YELLOW // Color for focused-but-not-selected
|
||||
val selectedColor = Color.WHITE // Color for selected (might be focused or not)
|
||||
|
||||
skinCards.forEach { (skinId, card) ->
|
||||
val skinInfo = getBlockSkins()[skinId] ?: return@forEach
|
||||
// Check unlock status based on the navigable list derived from level/unlocks
|
||||
val isUnlocked = skinIdList.contains(skinId)
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Keep locked skins visually distinct
|
||||
card.alpha = 0.5f
|
||||
card.cardElevation = 2f
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
card.scaleX = 1.0f // Reset scale
|
||||
card.scaleY = 1.0f
|
||||
return@forEach
|
||||
}
|
||||
|
||||
// Reset unlocked cards first
|
||||
card.alpha = 1.0f
|
||||
card.cardElevation = 4f
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
card.scaleX = 1.0f
|
||||
card.scaleY = 1.0f
|
||||
|
||||
val isSelected = (skinId == selectedSkin)
|
||||
val isFocused = (hasComponentFocus && skinId == focusedSkinId)
|
||||
|
||||
var borderColor = Color.TRANSPARENT
|
||||
var borderWidth = 0
|
||||
var elevation = 4f
|
||||
var scale = 1.0f
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = selectedColor
|
||||
borderWidth = 6 // Thick border for selected
|
||||
elevation = 12f // Higher elevation for selected
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
// Focused item gets a distinct border (unless it's also selected)
|
||||
if (!isSelected) {
|
||||
borderColor = focusColor
|
||||
borderWidth = 4 // Slightly thinner border for focused
|
||||
}
|
||||
elevation = 12f // Use high elevation for focus too
|
||||
scale = 1.1f // Scale up the focused item
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
card.scaleX = scale
|
||||
card.scaleY = scale
|
||||
|
||||
// Apply border and elevation
|
||||
if (borderWidth > 0) {
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(skinInfo.backgroundColor) // Use skin's background for the fill
|
||||
setStroke(borderWidth, borderColor)
|
||||
cornerRadius = 12f
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
} else {
|
||||
card.background = null // Ensure no border if not selected/focused
|
||||
}
|
||||
card.cardElevation = elevation
|
||||
}
|
||||
}
|
||||
|
||||
// Keep selectNextBlockSkin temporarily for compatibility, but it shouldn't be called by MainActivity anymore
|
||||
fun selectNextBlockSkin() {
|
||||
val allSkins = getBlockSkins().keys.toList()
|
||||
val currentIndex = allSkins.indexOf(selectedSkin)
|
||||
if (currentIndex == -1) return
|
||||
|
||||
var nextIndex = (currentIndex + 1) % allSkins.size
|
||||
while (nextIndex != currentIndex) {
|
||||
val nextSkin = allSkins[nextIndex]
|
||||
val skinInfo = getBlockSkins()[nextSkin] ?: continue
|
||||
val isEffectivelyUnlocked = skinCards[nextSkin]?.alpha == 1.0f // Basic check based on alpha
|
||||
|| playerLevel >= skinInfo.unlockLevel
|
||||
|
||||
if (isEffectivelyUnlocked) {
|
||||
// This method now just sets the internal focus and confirms
|
||||
focusedSkinId = nextSkin
|
||||
focusedIndex = skinIdList.indexOf(nextSkin) // Update index based on navigable list
|
||||
if (focusedIndex == -1) { // If not found in navigable list, reset focus
|
||||
focusedIndex = 0
|
||||
focusedSkinId = if (skinIdList.isNotEmpty()) skinIdList[0] else null
|
||||
}
|
||||
confirmSelection() // Confirm the selection
|
||||
return
|
||||
}
|
||||
nextIndex = (nextIndex + 1) % allSkins.size
|
||||
}
|
||||
}
|
||||
}
|
67
app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt
Normal file
67
app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt
Normal file
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
314
app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt
Normal file
314
app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt
Normal file
|
@ -0,0 +1,314 @@
|
|||
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
|
||||
|
||||
// Current theme
|
||||
private var currentTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
// 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<String>,
|
||||
themeId: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
) {
|
||||
// Update current theme
|
||||
currentTheme = themeId
|
||||
|
||||
// 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"
|
||||
|
||||
// Update level up text visibility
|
||||
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
||||
progressionTitle.visibility = if (newRewards.any { it.contains("Level") }) View.VISIBLE else View.GONE
|
||||
|
||||
// Start with initial animations
|
||||
AnimatorSet().apply {
|
||||
// Fade in the XP gain text
|
||||
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
|
||||
duration = 800
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}
|
||||
|
||||
// Set up the XP progress bar animation sequence
|
||||
val xpBarAnimator = ObjectAnimator.ofFloat(xpProgressBar, "alpha", 0f, 1f).apply {
|
||||
duration = 800
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}
|
||||
|
||||
// Play animations in sequence
|
||||
play(xpTextAnimator)
|
||||
play(xpBarAnimator).after(xpTextAnimator)
|
||||
start()
|
||||
}
|
||||
|
||||
// Set initial progress bar state
|
||||
xpProgressBar.setXPValues(playerLevel, currentXP - xpGained, xpForNextLevel)
|
||||
|
||||
// Animate the XP gain after a short delay
|
||||
postDelayed({
|
||||
xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel)
|
||||
}, 1000) // Increased delay to 1 second for better visual flow
|
||||
|
||||
// If there are new rewards, show them with animation after XP bar 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(600) // Increased duration for smoother animation
|
||||
.setStartDelay((i * 200).toLong()) // Increased delay between cards
|
||||
.setInterpolator(OvershootInterpolator())
|
||||
.start()
|
||||
}
|
||||
}, 2500) // Increased delay to wait for XP bar animation to finish
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
// Set background color based on current theme
|
||||
val backgroundColor = when (currentTheme) {
|
||||
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
|
||||
}
|
||||
setCardBackgroundColor(backgroundColor)
|
||||
|
||||
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) {
|
||||
currentTheme = themeId
|
||||
|
||||
// Get reference to the title text
|
||||
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
||||
val rewardsTitle = findViewById<TextView>(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
|
||||
}
|
||||
}
|
||||
|
||||
// Update XP progress bar theme color
|
||||
xpProgressBar.setThemeColor(xpThemeColor)
|
||||
|
||||
// Update reward card colors
|
||||
updateRewardCardColors(themeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update colors of existing reward cards to match the theme
|
||||
*/
|
||||
private fun updateRewardCardColors(themeId: String) {
|
||||
val backgroundColor = 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
|
||||
}
|
||||
|
||||
for (i in 0 until rewardsContainer.childCount) {
|
||||
val card = rewardsContainer.getChildAt(i) as? CardView
|
||||
card?.setCardBackgroundColor(backgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to handle continue action via gamepad
|
||||
*/
|
||||
fun performContinue() {
|
||||
continueButton.performClick()
|
||||
}
|
||||
}
|
415
app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt
Normal file
415
app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt
Normal file
|
@ -0,0 +1,415 @@
|
|||
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
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* 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 (persisted)
|
||||
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
// Theme cards map (themeId -> CardView)
|
||||
private val themeCards = mutableMapOf<String, CardView>()
|
||||
// Ordered list of theme IDs for navigation
|
||||
private val themeIdList = mutableListOf<String>()
|
||||
// Currently focused theme ID (for gamepad navigation within the selector)
|
||||
private var focusedThemeId: String? = null
|
||||
// Index of the currently focused theme in themeIdList
|
||||
private var focusedIndex: Int = -1
|
||||
// Flag indicating if the entire selector component has focus from the main menu
|
||||
private var hasComponentFocus: Boolean = false
|
||||
|
||||
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<String>, currentTheme: String) {
|
||||
// Clear existing theme cards and ID list
|
||||
themesGrid.removeAllViews()
|
||||
themeCards.clear()
|
||||
themeIdList.clear()
|
||||
|
||||
// Update selected theme
|
||||
selectedTheme = currentTheme
|
||||
focusedThemeId = currentTheme // Initially focus the selected theme
|
||||
focusedIndex = -1 // Reset index
|
||||
|
||||
// Get all possible themes and their details, sorted for consistent order
|
||||
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
||||
|
||||
// Add theme cards to the grid and build the ID list
|
||||
allThemes.forEach { (themeId, themeInfo) ->
|
||||
val isUnlocked = unlockedThemes.contains(themeId)
|
||||
val isSelected = themeId == selectedTheme
|
||||
|
||||
// Only add unlocked themes to the navigable list
|
||||
if (isUnlocked) {
|
||||
themeIdList.add(themeId)
|
||||
}
|
||||
|
||||
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
||||
themeCards[themeId] = themeCard
|
||||
themesGrid.addView(themeCard)
|
||||
|
||||
// Update focused index if this is the currently selected/focused theme
|
||||
if (isUnlocked && themeId == focusedThemeId) {
|
||||
focusedIndex = themeIdList.indexOf(themeId)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure focus index is valid if the previously focused theme is no longer available/unlocked
|
||||
if (focusedIndex == -1 && themeIdList.isNotEmpty()) {
|
||||
focusedIndex = 0
|
||||
focusedThemeId = themeIdList[0]
|
||||
}
|
||||
|
||||
// Apply initial focus highlight if the component has focus
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Clicking directly selects the theme
|
||||
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
|
||||
focusedThemeId = themeId
|
||||
focusedIndex = themeIdList.indexOf(themeId)
|
||||
confirmSelection() // Directly confirm click selection
|
||||
}
|
||||
}
|
||||
|
||||
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<String, ThemeInfo> {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the entire component has focus (from the parent menu).
|
||||
* Controls the outer border visibility.
|
||||
*/
|
||||
fun setHasFocus(hasFocus: Boolean) {
|
||||
hasComponentFocus = hasFocus
|
||||
// Update visual state based on component focus
|
||||
highlightFocusedCard() // Re-apply highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the next available theme.
|
||||
*/
|
||||
fun focusNextItem() {
|
||||
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
||||
|
||||
focusedIndex = (focusedIndex + 1) % themeIdList.size
|
||||
focusedThemeId = themeIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the previous available theme.
|
||||
*/
|
||||
fun focusPreviousItem() {
|
||||
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
||||
|
||||
focusedIndex = (focusedIndex - 1 + themeIdList.size) % themeIdList.size
|
||||
focusedThemeId = themeIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the currently focused theme as the selected theme.
|
||||
* Triggers the onThemeSelected callback.
|
||||
*/
|
||||
fun confirmSelection() {
|
||||
Log.d("ThemeSelector", "confirmSelection called. Focused theme: $focusedThemeId")
|
||||
if (focusedThemeId == null || focusedThemeId == selectedTheme) {
|
||||
// No change needed if nothing is focused,
|
||||
// or the focused item is already selected
|
||||
return
|
||||
}
|
||||
|
||||
// Update the selected theme
|
||||
selectedTheme = focusedThemeId!!
|
||||
|
||||
// Update visual states for all cards
|
||||
highlightFocusedCard() // This will now mark the new theme as selected
|
||||
|
||||
// Trigger the callback
|
||||
onThemeSelected?.invoke(selectedTheme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual highlight state of the theme cards based on
|
||||
* selection and internal focus.
|
||||
*/
|
||||
private fun highlightFocusedCard() {
|
||||
if (themeCards.isEmpty()) return
|
||||
|
||||
val focusColor = Color.YELLOW // Color for the focused-but-not-selected item
|
||||
val selectedColor = Color.WHITE // Color for the selected item (might be focused or not)
|
||||
|
||||
themeCards.forEach { (themeId, card) ->
|
||||
val themeInfo = getThemes()[themeId] ?: return@forEach
|
||||
val isUnlocked = themeIdList.contains(themeId) // Check if it's in the navigable list
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Keep locked themes visually distinct
|
||||
card.alpha = 0.5f
|
||||
card.cardElevation = 2f
|
||||
card.background = null // Remove any border/background
|
||||
card.setCardBackgroundColor(themeInfo.primaryColor)
|
||||
return@forEach // Skip further styling for locked themes
|
||||
}
|
||||
|
||||
// Reset unlocked cards first
|
||||
card.alpha = 1.0f
|
||||
card.cardElevation = 4f // Default elevation for unlocked cards
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(themeInfo.primaryColor)
|
||||
card.scaleX = 1.0f
|
||||
card.scaleY = 1.0f
|
||||
|
||||
val isSelected = (themeId == selectedTheme)
|
||||
val isFocused = (hasComponentFocus && themeId == focusedThemeId)
|
||||
|
||||
var borderColor = Color.TRANSPARENT
|
||||
var borderWidth = 0
|
||||
var elevation = 4f
|
||||
var scale = 1.0f
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = selectedColor
|
||||
borderWidth = 6 // Thick border for selected
|
||||
elevation = 12f // Higher elevation for selected
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
// Focused item gets a distinct border (unless it's also selected)
|
||||
if (!isSelected) {
|
||||
borderColor = focusColor
|
||||
borderWidth = 4 // Slightly thinner border for focused
|
||||
}
|
||||
elevation = 12f // Use high elevation for focus too
|
||||
scale = 1.1f // Scale up the focused item
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
card.scaleX = scale
|
||||
card.scaleY = scale
|
||||
|
||||
// Apply border and elevation
|
||||
if (borderWidth > 0) {
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(themeInfo.primaryColor)
|
||||
setStroke(borderWidth, borderColor)
|
||||
cornerRadius = 12f // Keep consistent corner radius
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
} else {
|
||||
card.background = null // Ensure no border if not selected/focused
|
||||
}
|
||||
card.cardElevation = elevation
|
||||
}
|
||||
}
|
||||
}
|
245
app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt
Normal file
245
app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt
Normal file
|
@ -0,0 +1,245 @@
|
|||
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
|
||||
progressPaint.color = color
|
||||
textPaint.color = 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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue