Fix: Correct namespace and applicationId in app build.gradle

This commit is contained in:
cmclark00 2025-04-01 07:51:52 -04:00
parent 5cf8aec02a
commit 5ace9d7fc5
25 changed files with 4 additions and 4 deletions

View 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
}
}
}

View 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
)
}
}

View 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()
}
}

View 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
}
}
}

View 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()
}
}