mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 09:05:20 +01:00
Compare commits
13 commits
0f1404d969
...
ae5ad1ed03
Author | SHA1 | Date | |
---|---|---|---|
|
ae5ad1ed03 | ||
|
ed7847ad70 | ||
|
843cae4b75 | ||
|
4812c99ae3 | ||
|
3e22d1c863 | ||
|
5c10c6d051 | ||
|
4166fe2b7a | ||
|
3b2f25c61f | ||
|
bae3a01087 | ||
|
4ea4ba7d1b | ||
|
dcc4cc28d5 | ||
|
d2554d5dfc | ||
|
ee2bb0883a |
23 changed files with 2128 additions and 311 deletions
|
@ -1,5 +1,6 @@
|
||||||
package com.mintris
|
package com.mintris
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
@ -14,6 +15,9 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
private lateinit var scoreText: TextView
|
private lateinit var scoreText: TextView
|
||||||
private lateinit var saveButton: Button
|
private lateinit var saveButton: Button
|
||||||
|
|
||||||
|
// Track if we already saved to prevent double-saving
|
||||||
|
private var hasSaved = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.high_score_entry)
|
setContentView(R.layout.high_score_entry)
|
||||||
|
@ -28,12 +32,28 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
scoreText.text = getString(R.string.score) + ": $score"
|
scoreText.text = getString(R.string.score) + ": $score"
|
||||||
|
|
||||||
saveButton.setOnClickListener {
|
saveButton.setOnClickListener {
|
||||||
|
// Only allow saving once
|
||||||
|
if (!hasSaved) {
|
||||||
val name = nameInput.text.toString().trim()
|
val name = nameInput.text.toString().trim()
|
||||||
if (name.isNotEmpty()) {
|
if (name.isNotEmpty()) {
|
||||||
|
hasSaved = true
|
||||||
val highScore = HighScore(name, score, level)
|
val highScore = HighScore(name, score, level)
|
||||||
highScoreManager.addHighScore(highScore)
|
highScoreManager.addHighScore(highScore)
|
||||||
|
|
||||||
|
// Set result and finish
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent accidental back button press from causing issues
|
||||||
|
override fun onBackPressed() {
|
||||||
|
// If they haven't saved yet, consider it a cancel
|
||||||
|
if (!hasSaved) {
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
package com.mintris
|
package com.mintris
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.VibrationEffect
|
import android.os.VibrationEffect
|
||||||
|
@ -11,6 +14,7 @@ import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.mintris.databinding.ActivityMainBinding
|
import com.mintris.databinding.ActivityMainBinding
|
||||||
import com.mintris.game.GameHaptics
|
import com.mintris.game.GameHaptics
|
||||||
import com.mintris.game.GameView
|
import com.mintris.game.GameView
|
||||||
|
@ -20,10 +24,14 @@ import android.view.HapticFeedbackConstants
|
||||||
import com.mintris.model.GameBoard
|
import com.mintris.model.GameBoard
|
||||||
import com.mintris.audio.GameMusic
|
import com.mintris.audio.GameMusic
|
||||||
import com.mintris.model.HighScoreManager
|
import com.mintris.model.HighScoreManager
|
||||||
import android.content.Intent
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import com.mintris.model.StatsManager
|
import com.mintris.model.StatsManager
|
||||||
|
import com.mintris.ui.ProgressionScreen
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import android.graphics.Color
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@ -36,6 +44,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var titleScreen: TitleScreen
|
private lateinit var titleScreen: TitleScreen
|
||||||
private lateinit var highScoreManager: HighScoreManager
|
private lateinit var highScoreManager: HighScoreManager
|
||||||
private lateinit var statsManager: StatsManager
|
private lateinit var statsManager: StatsManager
|
||||||
|
private lateinit var progressionManager: PlayerProgressionManager
|
||||||
|
private lateinit var progressionScreen: ProgressionScreen
|
||||||
|
|
||||||
// Game state
|
// Game state
|
||||||
private var isSoundEnabled = true
|
private var isSoundEnabled = true
|
||||||
|
@ -46,8 +56,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
private var currentLevel = 1
|
private var currentLevel = 1
|
||||||
private var gameStartTime: Long = 0
|
private var gameStartTime: Long = 0
|
||||||
private var piecesPlaced: Int = 0
|
private var piecesPlaced: Int = 0
|
||||||
|
private var currentTheme = PlayerProgressionManager.THEME_CLASSIC
|
||||||
|
|
||||||
|
// Activity result launcher for high score entry
|
||||||
|
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// Register activity result launcher for high score entry
|
||||||
|
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
// No matter what the result is, we just show the game over container
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
|
binding.gameOverContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
@ -60,11 +81,30 @@ class MainActivity : AppCompatActivity() {
|
||||||
gameMusic = GameMusic(this)
|
gameMusic = GameMusic(this)
|
||||||
highScoreManager = HighScoreManager(this)
|
highScoreManager = HighScoreManager(this)
|
||||||
statsManager = StatsManager(this)
|
statsManager = StatsManager(this)
|
||||||
|
progressionManager = PlayerProgressionManager(this)
|
||||||
|
|
||||||
|
// Load and apply theme preference
|
||||||
|
currentTheme = loadThemePreference()
|
||||||
|
applyTheme(currentTheme)
|
||||||
|
|
||||||
// Set up game view
|
// Set up game view
|
||||||
gameView.setGameBoard(gameBoard)
|
gameView.setGameBoard(gameBoard)
|
||||||
gameView.setHaptics(gameHaptics)
|
gameView.setHaptics(gameHaptics)
|
||||||
|
|
||||||
|
// Set up progression screen
|
||||||
|
progressionScreen = binding.progressionScreen
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
|
progressionScreen.onContinue = {
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
|
binding.gameOverContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up theme selector
|
||||||
|
val themeSelector = binding.themeSelector
|
||||||
|
themeSelector.onThemeSelected = { themeId ->
|
||||||
|
applyTheme(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up title screen
|
// Set up title screen
|
||||||
titleScreen.onStartGame = {
|
titleScreen.onStartGame = {
|
||||||
titleScreen.visibility = View.GONE
|
titleScreen.visibility = View.GONE
|
||||||
|
@ -244,6 +284,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
level = currentLevel
|
level = currentLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Calculate XP earned
|
||||||
|
val xpGained = progressionManager.calculateGameXP(
|
||||||
|
score = score,
|
||||||
|
lines = gameBoard.lines,
|
||||||
|
level = currentLevel,
|
||||||
|
gameTime = gameTime,
|
||||||
|
tetrisCount = statsManager.getSessionTetrises(),
|
||||||
|
perfectClearCount = 0 // Implement perfect clear tracking if needed
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add XP and check for rewards
|
||||||
|
val newRewards = progressionManager.addXP(xpGained)
|
||||||
|
|
||||||
// End session and save stats
|
// End session and save stats
|
||||||
statsManager.endSession()
|
statsManager.endSession()
|
||||||
|
|
||||||
|
@ -263,19 +316,49 @@ class MainActivity : AppCompatActivity() {
|
||||||
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
|
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
|
||||||
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
|
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
|
||||||
|
|
||||||
// Check if this is a high score
|
// Flag to track if high score screen will be shown
|
||||||
|
var showingHighScore = false
|
||||||
|
|
||||||
|
// Show progression screen first with XP animation
|
||||||
|
binding.gameOverContainer.visibility = View.GONE
|
||||||
|
progressionScreen.visibility = View.VISIBLE
|
||||||
|
progressionScreen.applyTheme(currentTheme)
|
||||||
|
progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme)
|
||||||
|
|
||||||
|
// Override the continue button behavior if high score needs to be shown
|
||||||
|
val originalOnContinue = progressionScreen.onContinue
|
||||||
|
|
||||||
|
progressionScreen.onContinue = {
|
||||||
|
// If this is a high score, show high score entry screen
|
||||||
if (highScoreManager.isHighScore(score)) {
|
if (highScoreManager.isHighScore(score)) {
|
||||||
|
showingHighScore = true
|
||||||
|
showHighScoreEntry(score)
|
||||||
|
} else {
|
||||||
|
// Just show game over screen normally
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
|
binding.gameOverContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
// Update theme selector if new themes were unlocked
|
||||||
|
if (newRewards.any { it.contains("Theme") }) {
|
||||||
|
updateThemeSelector()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vibrate to indicate game over
|
||||||
|
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show high score entry screen
|
||||||
|
*/
|
||||||
|
private fun showHighScoreEntry(score: Int) {
|
||||||
val intent = Intent(this, HighScoreEntryActivity::class.java).apply {
|
val intent = Intent(this, HighScoreEntryActivity::class.java).apply {
|
||||||
putExtra("score", score)
|
putExtra("score", score)
|
||||||
putExtra("level", currentLevel)
|
putExtra("level", currentLevel)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
// Use the launcher instead of startActivity
|
||||||
}
|
highScoreEntryLauncher.launch(intent)
|
||||||
|
|
||||||
binding.gameOverContainer.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
// Vibrate to indicate game over
|
|
||||||
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -283,6 +366,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
*/
|
*/
|
||||||
private fun hideGameOver() {
|
private fun hideGameOver() {
|
||||||
binding.gameOverContainer.visibility = View.GONE
|
binding.gameOverContainer.visibility = View.GONE
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -292,6 +376,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
binding.pauseContainer.visibility = View.VISIBLE
|
binding.pauseContainer.visibility = View.VISIBLE
|
||||||
binding.pauseStartButton.visibility = View.VISIBLE
|
binding.pauseStartButton.visibility = View.VISIBLE
|
||||||
binding.resumeButton.visibility = View.GONE
|
binding.resumeButton.visibility = View.GONE
|
||||||
|
|
||||||
|
// Update level badge
|
||||||
|
binding.pauseLevelBadge.setLevel(progressionManager.getPlayerLevel())
|
||||||
|
binding.pauseLevelBadge.setThemeColor(getThemeColor(currentTheme))
|
||||||
|
|
||||||
|
// Update theme selector
|
||||||
|
updateThemeSelector()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -346,6 +437,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
gameStartTime = System.currentTimeMillis()
|
gameStartTime = System.currentTimeMillis()
|
||||||
piecesPlaced = 0
|
piecesPlaced = 0
|
||||||
statsManager.startNewSession()
|
statsManager.startNewSession()
|
||||||
|
progressionManager.startNewSession()
|
||||||
|
gameBoard.updateLevel(selectedLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restartGame() {
|
private fun restartGame() {
|
||||||
|
@ -378,6 +471,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) {
|
if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) {
|
||||||
resumeGame()
|
resumeGame()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update theme selector with available themes when pause screen appears
|
||||||
|
updateThemeSelector()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -395,6 +491,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
binding.gameOverContainer.visibility = View.GONE
|
binding.gameOverContainer.visibility = View.GONE
|
||||||
binding.pauseContainer.visibility = View.GONE
|
binding.pauseContainer.visibility = View.GONE
|
||||||
titleScreen.visibility = View.VISIBLE
|
titleScreen.visibility = View.VISIBLE
|
||||||
|
titleScreen.applyTheme(currentTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -404,4 +501,104 @@ class MainActivity : AppCompatActivity() {
|
||||||
val intent = Intent(this, HighScoresActivity::class.java)
|
val intent = Intent(this, HighScoresActivity::class.java)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the theme selector with unlocked themes
|
||||||
|
*/
|
||||||
|
private fun updateThemeSelector() {
|
||||||
|
binding.themeSelector.updateThemes(
|
||||||
|
unlockedThemes = progressionManager.getUnlockedThemes(),
|
||||||
|
currentTheme = currentTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a theme to the game
|
||||||
|
*/
|
||||||
|
private fun applyTheme(themeId: String) {
|
||||||
|
// Only apply if the theme is unlocked
|
||||||
|
if (!progressionManager.isThemeUnlocked(themeId)) return
|
||||||
|
|
||||||
|
// Save the selected theme
|
||||||
|
currentTheme = themeId
|
||||||
|
saveThemePreference(themeId)
|
||||||
|
|
||||||
|
// Apply theme to title screen if it's visible
|
||||||
|
if (titleScreen.visibility == View.VISIBLE) {
|
||||||
|
titleScreen.applyTheme(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme colors based on theme ID
|
||||||
|
when (themeId) {
|
||||||
|
PlayerProgressionManager.THEME_CLASSIC -> {
|
||||||
|
// Default black theme
|
||||||
|
binding.root.setBackgroundColor(Color.BLACK)
|
||||||
|
}
|
||||||
|
PlayerProgressionManager.THEME_NEON -> {
|
||||||
|
// Neon theme with dark purple background
|
||||||
|
binding.root.setBackgroundColor(Color.parseColor("#0D0221"))
|
||||||
|
}
|
||||||
|
PlayerProgressionManager.THEME_MONOCHROME -> {
|
||||||
|
// Monochrome dark gray
|
||||||
|
binding.root.setBackgroundColor(Color.parseColor("#1A1A1A"))
|
||||||
|
}
|
||||||
|
PlayerProgressionManager.THEME_RETRO -> {
|
||||||
|
// Retro arcade theme
|
||||||
|
binding.root.setBackgroundColor(Color.parseColor("#3F2832"))
|
||||||
|
}
|
||||||
|
PlayerProgressionManager.THEME_MINIMALIST -> {
|
||||||
|
// Minimalist white theme
|
||||||
|
binding.root.setBackgroundColor(Color.WHITE)
|
||||||
|
|
||||||
|
// Update text colors for visibility
|
||||||
|
binding.scoreText.setTextColor(Color.BLACK)
|
||||||
|
binding.currentLevelText.setTextColor(Color.BLACK)
|
||||||
|
binding.linesText.setTextColor(Color.BLACK)
|
||||||
|
binding.comboText.setTextColor(Color.BLACK)
|
||||||
|
}
|
||||||
|
PlayerProgressionManager.THEME_GALAXY -> {
|
||||||
|
// Galaxy dark blue theme
|
||||||
|
binding.root.setBackgroundColor(Color.parseColor("#0B0C10"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme to progression screen if it's visible and initialized
|
||||||
|
if (::progressionScreen.isInitialized && progressionScreen.visibility == View.VISIBLE) {
|
||||||
|
progressionScreen.applyTheme(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the game view to apply theme
|
||||||
|
gameView.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the selected theme in preferences
|
||||||
|
*/
|
||||||
|
private fun saveThemePreference(themeId: String) {
|
||||||
|
val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putString("selected_theme", themeId).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the saved theme preference
|
||||||
|
*/
|
||||||
|
private fun loadThemePreference(): String {
|
||||||
|
val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE)
|
||||||
|
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate color for the current theme
|
||||||
|
*/
|
||||||
|
private fun getThemeColor(themeId: String): Int {
|
||||||
|
return when (themeId) {
|
||||||
|
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
|
||||||
|
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
|
||||||
|
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
|
||||||
|
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
|
||||||
|
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
|
||||||
|
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
|
||||||
|
else -> Color.WHITE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ package com.mintris
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.mintris.databinding.ActivityStatsBinding
|
import com.mintris.databinding.ActivityStatsBinding
|
||||||
import com.mintris.model.StatsManager
|
import com.mintris.model.StatsManager
|
||||||
|
@ -25,9 +26,26 @@ class StatsActivity : AppCompatActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up reset stats button
|
||||||
|
binding.resetStatsButton.setOnClickListener {
|
||||||
|
showResetConfirmationDialog()
|
||||||
|
}
|
||||||
|
|
||||||
updateStats()
|
updateStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showResetConfirmationDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("Reset Stats")
|
||||||
|
.setMessage("Are you sure you want to reset all your stats? This cannot be undone.")
|
||||||
|
.setPositiveButton("Reset") { _, _ ->
|
||||||
|
statsManager.resetStats()
|
||||||
|
updateStats()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateStats() {
|
private fun updateStats() {
|
||||||
// Format time duration
|
// Format time duration
|
||||||
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||||
|
|
|
@ -9,10 +9,15 @@ import com.mintris.R
|
||||||
|
|
||||||
class GameMusic(private val context: Context) {
|
class GameMusic(private val context: Context) {
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var isEnabled = false
|
private var isEnabled = true
|
||||||
|
private var isPrepared = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
try {
|
||||||
setupMediaPlayer()
|
setupMediaPlayer()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("GameMusic", "Error initializing: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupMediaPlayer() {
|
private fun setupMediaPlayer() {
|
||||||
|
@ -31,42 +36,47 @@ class GameMusic(private val context: Context) {
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
isPrepared = true
|
||||||
}
|
}
|
||||||
Log.d("GameMusic", "MediaPlayer setup complete")
|
Log.d("GameMusic", "MediaPlayer setup complete")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error setting up MediaPlayer", e)
|
Log.e("GameMusic", "Error setting up MediaPlayer", e)
|
||||||
|
mediaPlayer = null
|
||||||
|
isPrepared = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
if (isEnabled && mediaPlayer != null && isPrepared) {
|
||||||
try {
|
try {
|
||||||
Log.d("GameMusic", "Starting music playback, isEnabled: $isEnabled")
|
Log.d("GameMusic", "Starting music playback, isEnabled: $isEnabled")
|
||||||
if (isEnabled && mediaPlayer?.isPlaying != true) {
|
|
||||||
mediaPlayer?.start()
|
mediaPlayer?.start()
|
||||||
Log.d("GameMusic", "Music playback started")
|
Log.d("GameMusic", "Music playback started")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error starting music", e)
|
Log.e("GameMusic", "Error starting music: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
fun pause() {
|
||||||
try {
|
try {
|
||||||
Log.d("GameMusic", "Pausing music playback")
|
Log.d("GameMusic", "Pausing music playback")
|
||||||
|
if (mediaPlayer?.isPlaying == true) {
|
||||||
mediaPlayer?.pause()
|
mediaPlayer?.pause()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error pausing music", e)
|
Log.e("GameMusic", "Error pausing music: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resume() {
|
fun resume() {
|
||||||
|
if (isEnabled && mediaPlayer != null && isPrepared) {
|
||||||
try {
|
try {
|
||||||
Log.d("GameMusic", "Resuming music playback")
|
Log.d("GameMusic", "Resuming music playback")
|
||||||
if (isEnabled && mediaPlayer?.isPlaying != true) {
|
|
||||||
mediaPlayer?.start()
|
mediaPlayer?.start()
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error resuming music", e)
|
Log.e("GameMusic", "Error resuming music: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,10 +93,11 @@ class GameMusic(private val context: Context) {
|
||||||
fun setEnabled(enabled: Boolean) {
|
fun setEnabled(enabled: Boolean) {
|
||||||
Log.d("GameMusic", "Setting music enabled: $enabled")
|
Log.d("GameMusic", "Setting music enabled: $enabled")
|
||||||
isEnabled = enabled
|
isEnabled = enabled
|
||||||
if (enabled) {
|
|
||||||
start()
|
if (!enabled && mediaPlayer?.isPlaying == true) {
|
||||||
} else {
|
|
||||||
pause()
|
pause()
|
||||||
|
} else if (enabled && mediaPlayer != null && isPrepared) {
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,8 +108,9 @@ class GameMusic(private val context: Context) {
|
||||||
Log.d("GameMusic", "Releasing MediaPlayer")
|
Log.d("GameMusic", "Releasing MediaPlayer")
|
||||||
mediaPlayer?.release()
|
mediaPlayer?.release()
|
||||||
mediaPlayer = null
|
mediaPlayer = null
|
||||||
|
isPrepared = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error releasing MediaPlayer", e)
|
Log.e("GameMusic", "Error releasing music: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -133,6 +133,13 @@ class GameView @JvmOverloads constructor(
|
||||||
private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
|
private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
|
||||||
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
|
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
|
||||||
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
|
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
|
||||||
|
private var lockedDirection: Direction? = null // Track the locked movement direction
|
||||||
|
private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size
|
||||||
|
private val directionLockThreshold = 1.5f // Threshold for direction lock relative to block size
|
||||||
|
|
||||||
|
private enum class Direction {
|
||||||
|
HORIZONTAL, VERTICAL
|
||||||
|
}
|
||||||
|
|
||||||
// Callback for game events
|
// Callback for game events
|
||||||
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
|
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
|
||||||
|
@ -578,6 +585,7 @@ class GameView @JvmOverloads constructor(
|
||||||
startY = event.y
|
startY = event.y
|
||||||
lastTouchX = event.x
|
lastTouchX = event.x
|
||||||
lastTouchY = event.y
|
lastTouchY = event.y
|
||||||
|
lockedDirection = null // Reset direction lock
|
||||||
|
|
||||||
// Check for double tap (rotate)
|
// Check for double tap (rotate)
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
|
@ -597,27 +605,43 @@ class GameView @JvmOverloads constructor(
|
||||||
val deltaY = event.y - lastTouchY
|
val deltaY = event.y - lastTouchY
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
// Horizontal movement (left/right) with reduced threshold
|
// Determine movement direction if not locked
|
||||||
if (abs(deltaX) > blockSize * 0.5f) { // Reduced from 1.0f for more responsive movement
|
if (lockedDirection == null) {
|
||||||
|
val absDeltaX = abs(deltaX)
|
||||||
|
val absDeltaY = abs(deltaY)
|
||||||
|
|
||||||
|
// Check if movement exceeds threshold
|
||||||
|
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) {
|
||||||
|
// Determine dominant direction
|
||||||
|
if (absDeltaX > absDeltaY * directionLockThreshold) {
|
||||||
|
lockedDirection = Direction.HORIZONTAL
|
||||||
|
} else if (absDeltaY > absDeltaX * directionLockThreshold) {
|
||||||
|
lockedDirection = Direction.VERTICAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle movement based on locked direction
|
||||||
|
when (lockedDirection) {
|
||||||
|
Direction.HORIZONTAL -> {
|
||||||
|
if (abs(deltaX) > blockSize * minMovementThreshold) {
|
||||||
if (deltaX > 0) {
|
if (deltaX > 0) {
|
||||||
gameBoard.moveRight()
|
gameBoard.moveRight()
|
||||||
} else {
|
} else {
|
||||||
gameBoard.moveLeft()
|
gameBoard.moveLeft()
|
||||||
}
|
}
|
||||||
lastTouchX = event.x
|
lastTouchX = event.x
|
||||||
// Add haptic feedback for movement with cooldown
|
|
||||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||||
gameHaptics?.vibrateForPieceMove()
|
gameHaptics?.vibrateForPieceMove()
|
||||||
lastMoveTime = currentTime
|
lastMoveTime = currentTime
|
||||||
}
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Vertical movement (soft drop) with reduced threshold
|
Direction.VERTICAL -> {
|
||||||
if (deltaY > blockSize * 0.25f) { // Reduced from 0.5f for more responsive soft drop
|
if (deltaY > blockSize * minMovementThreshold) {
|
||||||
gameBoard.moveDown()
|
gameBoard.moveDown()
|
||||||
lastTouchY = event.y
|
lastTouchY = event.y
|
||||||
// Add haptic feedback for movement with cooldown
|
|
||||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||||
gameHaptics?.vibrateForPieceMove()
|
gameHaptics?.vibrateForPieceMove()
|
||||||
lastMoveTime = currentTime
|
lastMoveTime = currentTime
|
||||||
|
@ -625,6 +649,11 @@ class GameView @JvmOverloads constructor(
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
null -> {
|
||||||
|
// No direction lock yet, don't process movement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
// Calculate movement speed for potential fling detection
|
// Calculate movement speed for potential fling detection
|
||||||
|
@ -647,6 +676,9 @@ class GameView @JvmOverloads constructor(
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset direction lock
|
||||||
|
lockedDirection = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import java.util.Random
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.mintris.model.HighScoreManager
|
import com.mintris.model.HighScoreManager
|
||||||
import com.mintris.model.HighScore
|
import com.mintris.model.HighScore
|
||||||
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import androidx.core.graphics.withTranslation
|
import androidx.core.graphics.withTranslation
|
||||||
import androidx.core.graphics.withScale
|
import androidx.core.graphics.withScale
|
||||||
|
@ -45,6 +46,9 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
// Callback for when the user touches the screen
|
// Callback for when the user touches the screen
|
||||||
var onStartGame: (() -> Unit)? = null
|
var onStartGame: (() -> Unit)? = null
|
||||||
|
|
||||||
|
// Theme color
|
||||||
|
private var themeColor = Color.WHITE
|
||||||
|
|
||||||
// Define tetromino shapes (I, O, T, S, Z, J, L)
|
// Define tetromino shapes (I, O, T, S, Z, J, L)
|
||||||
private val tetrominoShapes = arrayOf(
|
private val tetrominoShapes = arrayOf(
|
||||||
// I
|
// I
|
||||||
|
@ -116,7 +120,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
// "Touch to start" text settings
|
// "Touch to start" text settings
|
||||||
promptPaint.apply {
|
promptPaint.apply {
|
||||||
color = Color.WHITE
|
color = Color.WHITE
|
||||||
textSize = 40f
|
textSize = 50f
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
|
@ -126,9 +130,9 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
// High scores text settings
|
// High scores text settings
|
||||||
highScorePaint.apply {
|
highScorePaint.apply {
|
||||||
color = Color.WHITE
|
color = Color.WHITE
|
||||||
textSize = 35f
|
textSize = 70f
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.LEFT // Changed to LEFT alignment
|
||||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
alpha = 200
|
alpha = 200
|
||||||
}
|
}
|
||||||
|
@ -241,9 +245,17 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
||||||
val highScoreY = height * 0.5f
|
val highScoreY = height * 0.5f
|
||||||
if (highScores.isNotEmpty()) {
|
if (highScores.isNotEmpty()) {
|
||||||
|
// Calculate the starting X position to center the entire block of scores
|
||||||
|
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
||||||
|
val startX = (width - maxScoreWidth) / 2
|
||||||
|
|
||||||
highScores.forEachIndexed { index: Int, score: HighScore ->
|
highScores.forEachIndexed { index: Int, score: HighScore ->
|
||||||
val y = highScoreY + (index * 35f)
|
val y = highScoreY + (index * 80f)
|
||||||
canvas.drawText("${index + 1}. ${score.name}: ${score.score}", width / 2f, y, highScorePaint)
|
// Pad the rank number to ensure alignment
|
||||||
|
val rank = (index + 1).toString().padStart(2, ' ')
|
||||||
|
// Pad the name to ensure score alignment
|
||||||
|
val paddedName = score.name.padEnd(8, ' ')
|
||||||
|
canvas.drawText("$rank. $paddedName ${score.score}", startX, y, highScorePaint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,4 +316,40 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
onStartGame?.invoke()
|
onStartGame?.invoke()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a theme to the title screen
|
||||||
|
*/
|
||||||
|
fun applyTheme(themeId: String) {
|
||||||
|
// Get theme color based on theme ID
|
||||||
|
themeColor = when (themeId) {
|
||||||
|
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
|
||||||
|
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
|
||||||
|
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
|
||||||
|
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
|
||||||
|
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
|
||||||
|
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
|
||||||
|
else -> Color.WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update paint colors
|
||||||
|
titlePaint.color = themeColor
|
||||||
|
promptPaint.color = themeColor
|
||||||
|
highScorePaint.color = themeColor
|
||||||
|
paint.color = themeColor
|
||||||
|
glowPaint.color = themeColor
|
||||||
|
|
||||||
|
// Update background color
|
||||||
|
setBackgroundColor(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
|
||||||
|
})
|
||||||
|
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -29,6 +29,7 @@ class GameBoard(
|
||||||
// Game state
|
// Game state
|
||||||
var score = 0
|
var score = 0
|
||||||
var level = 1
|
var level = 1
|
||||||
|
var startingLevel = 1 // Add this line to track the starting level
|
||||||
var lines = 0
|
var lines = 0
|
||||||
var isGameOver = false
|
var isGameOver = false
|
||||||
var isHardDropInProgress = false // Make public
|
var isHardDropInProgress = false // Make public
|
||||||
|
@ -70,7 +71,7 @@ class GameBoard(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take the next piece from the bag
|
// Take the next piece from the bag
|
||||||
nextPiece = Tetromino(bag.removeFirst())
|
nextPiece = Tetromino(bag.removeAt(0))
|
||||||
onNextPieceChanged?.invoke()
|
onNextPieceChanged?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,7 +440,8 @@ class GameBoard(
|
||||||
|
|
||||||
// Update lines cleared and level
|
// Update lines cleared and level
|
||||||
lines += clearedLines
|
lines += clearedLines
|
||||||
level = (lines / 10) + 1
|
// Calculate level based on lines cleared, but ensure it's never below the starting level
|
||||||
|
level = Math.max((lines / 10) + 1, startingLevel)
|
||||||
|
|
||||||
// Update game speed based on level (NES formula)
|
// Update game speed based on level (NES formula)
|
||||||
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
||||||
|
@ -519,6 +521,7 @@ class GameBoard(
|
||||||
*/
|
*/
|
||||||
fun updateLevel(newLevel: Int) {
|
fun updateLevel(newLevel: Int) {
|
||||||
level = newLevel.coerceIn(1, 20)
|
level = newLevel.coerceIn(1, 20)
|
||||||
|
startingLevel = level // Store the starting level
|
||||||
// Update game speed based on level (NES formula)
|
// Update game speed based on level (NES formula)
|
||||||
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
||||||
}
|
}
|
||||||
|
@ -546,10 +549,10 @@ class GameBoard(
|
||||||
|
|
||||||
// Reset game state
|
// Reset game state
|
||||||
score = 0
|
score = 0
|
||||||
level = 1
|
level = startingLevel // Use starting level instead of resetting to 1
|
||||||
lines = 0
|
lines = 0
|
||||||
isGameOver = false
|
isGameOver = false
|
||||||
dropInterval = 1000L // Reset to level 1 speed
|
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() // Set speed based on current level
|
||||||
|
|
||||||
// Reset scoring state
|
// Reset scoring state
|
||||||
combo = 0
|
combo = 0
|
||||||
|
|
345
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
Normal file
345
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
package com.mintris.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.mintris.R
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages player progression, experience points, and unlockable rewards
|
||||||
|
*/
|
||||||
|
class PlayerProgressionManager(context: Context) {
|
||||||
|
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// Player level and XP
|
||||||
|
private var playerLevel: Int = 1
|
||||||
|
private var playerXP: Long = 0
|
||||||
|
private var totalXPEarned: Long = 0
|
||||||
|
|
||||||
|
// Track unlocked rewards
|
||||||
|
private val unlockedThemes = mutableSetOf<String>()
|
||||||
|
private val unlockedBlocks = mutableSetOf<String>()
|
||||||
|
private val unlockedPowers = mutableSetOf<String>()
|
||||||
|
private val unlockedBadges = mutableSetOf<String>()
|
||||||
|
|
||||||
|
// XP gained in the current session
|
||||||
|
private var sessionXPGained: Long = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load player progression data from shared preferences
|
||||||
|
*/
|
||||||
|
private fun loadProgress() {
|
||||||
|
playerLevel = prefs.getInt(KEY_PLAYER_LEVEL, 1)
|
||||||
|
playerXP = prefs.getLong(KEY_PLAYER_XP, 0)
|
||||||
|
totalXPEarned = prefs.getLong(KEY_TOTAL_XP_EARNED, 0)
|
||||||
|
|
||||||
|
// Load unlocked rewards
|
||||||
|
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
|
||||||
|
val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, setOf()) ?: setOf()
|
||||||
|
val powersSet = prefs.getStringSet(KEY_UNLOCKED_POWERS, setOf()) ?: setOf()
|
||||||
|
val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf()
|
||||||
|
|
||||||
|
unlockedThemes.addAll(themesSet)
|
||||||
|
unlockedBlocks.addAll(blocksSet)
|
||||||
|
unlockedPowers.addAll(powersSet)
|
||||||
|
unlockedBadges.addAll(badgesSet)
|
||||||
|
|
||||||
|
// Add default theme if nothing is unlocked
|
||||||
|
if (unlockedThemes.isEmpty()) {
|
||||||
|
unlockedThemes.add(THEME_CLASSIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save player progression data to shared preferences
|
||||||
|
*/
|
||||||
|
private fun saveProgress() {
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(KEY_PLAYER_LEVEL, playerLevel)
|
||||||
|
.putLong(KEY_PLAYER_XP, playerXP)
|
||||||
|
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
|
||||||
|
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
|
||||||
|
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
|
||||||
|
.putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers)
|
||||||
|
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate XP required to reach a specific level
|
||||||
|
*/
|
||||||
|
fun calculateXPForLevel(level: Int): Long {
|
||||||
|
return (BASE_XP * level.toDouble().pow(XP_CURVE_FACTOR)).roundToInt().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total XP required to reach a certain level from level 1
|
||||||
|
*/
|
||||||
|
fun calculateTotalXPForLevel(level: Int): Long {
|
||||||
|
var totalXP = 0L
|
||||||
|
for (lvl in 1 until level) {
|
||||||
|
totalXP += calculateXPForLevel(lvl)
|
||||||
|
}
|
||||||
|
return totalXP
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate XP from a game session based on score, lines, level, etc.
|
||||||
|
*/
|
||||||
|
fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long,
|
||||||
|
tetrisCount: Int, perfectClearCount: Int): Long {
|
||||||
|
// Base XP from score with level multiplier
|
||||||
|
val scoreXP = (score * (1 + LEVEL_MULTIPLIER * level)).toLong()
|
||||||
|
|
||||||
|
// XP from lines cleared
|
||||||
|
val linesXP = lines * XP_PER_LINE
|
||||||
|
|
||||||
|
// XP from special moves
|
||||||
|
val tetrisBonus = tetrisCount * TETRIS_XP_BONUS
|
||||||
|
val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS
|
||||||
|
|
||||||
|
// Time bonus (to reward longer gameplay)
|
||||||
|
val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE // XP per minute played
|
||||||
|
|
||||||
|
// Calculate total XP
|
||||||
|
return scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add XP to the player and handle level-ups
|
||||||
|
* Returns a list of newly unlocked rewards
|
||||||
|
*/
|
||||||
|
fun addXP(xpAmount: Long): List<String> {
|
||||||
|
sessionXPGained = xpAmount
|
||||||
|
playerXP += xpAmount
|
||||||
|
totalXPEarned += xpAmount
|
||||||
|
|
||||||
|
val newRewards = mutableListOf<String>()
|
||||||
|
val oldLevel = playerLevel
|
||||||
|
|
||||||
|
// Check for level ups
|
||||||
|
var xpForNextLevel = calculateXPForLevel(playerLevel)
|
||||||
|
while (playerXP >= xpForNextLevel) {
|
||||||
|
playerXP -= xpForNextLevel
|
||||||
|
playerLevel++
|
||||||
|
|
||||||
|
// Check for new rewards at this level
|
||||||
|
val levelRewards = checkLevelRewards(playerLevel)
|
||||||
|
newRewards.addAll(levelRewards)
|
||||||
|
|
||||||
|
// Calculate XP needed for the next level
|
||||||
|
xpForNextLevel = calculateXPForLevel(playerLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save progress if there were any changes
|
||||||
|
if (oldLevel != playerLevel || newRewards.isNotEmpty()) {
|
||||||
|
saveProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRewards
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the player unlocked new rewards at the current level
|
||||||
|
*/
|
||||||
|
private fun checkLevelRewards(level: Int): List<String> {
|
||||||
|
val newRewards = mutableListOf<String>()
|
||||||
|
|
||||||
|
// Check for theme unlocks
|
||||||
|
when (level) {
|
||||||
|
5 -> {
|
||||||
|
if (unlockedThemes.add(THEME_NEON)) {
|
||||||
|
newRewards.add("Unlocked Neon Theme!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
10 -> {
|
||||||
|
if (unlockedThemes.add(THEME_MONOCHROME)) {
|
||||||
|
newRewards.add("Unlocked Monochrome Theme!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
15 -> {
|
||||||
|
if (unlockedThemes.add(THEME_RETRO)) {
|
||||||
|
newRewards.add("Unlocked Retro Arcade Theme!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
20 -> {
|
||||||
|
if (unlockedThemes.add(THEME_MINIMALIST)) {
|
||||||
|
newRewards.add("Unlocked Minimalist Theme!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
25 -> {
|
||||||
|
if (unlockedThemes.add(THEME_GALAXY)) {
|
||||||
|
newRewards.add("Unlocked Galaxy Theme!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for power unlocks
|
||||||
|
when (level) {
|
||||||
|
8 -> {
|
||||||
|
if (unlockedPowers.add(POWER_FREEZE_TIME)) {
|
||||||
|
newRewards.add("Unlocked Freeze Time Power!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
12 -> {
|
||||||
|
if (unlockedPowers.add(POWER_BLOCK_SWAP)) {
|
||||||
|
newRewards.add("Unlocked Block Swap Power!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
18 -> {
|
||||||
|
if (unlockedPowers.add(POWER_SAFE_LANDING)) {
|
||||||
|
newRewards.add("Unlocked Safe Landing Power!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
30 -> {
|
||||||
|
if (unlockedPowers.add(POWER_PERFECT_CLEAR)) {
|
||||||
|
newRewards.add("Unlocked Perfect Clear Power!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for block skin unlocks
|
||||||
|
if (level % 7 == 0 && level <= 35) {
|
||||||
|
val blockSkin = "block_skin_${level / 7}"
|
||||||
|
if (unlockedBlocks.add(blockSkin)) {
|
||||||
|
newRewards.add("Unlocked New Block Skin!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRewards
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new progression session
|
||||||
|
*/
|
||||||
|
fun startNewSession() {
|
||||||
|
sessionXPGained = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
fun getPlayerLevel(): Int = playerLevel
|
||||||
|
fun getCurrentXP(): Long = playerXP
|
||||||
|
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
|
||||||
|
fun getSessionXPGained(): Long = sessionXPGained
|
||||||
|
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
|
||||||
|
fun getUnlockedPowers(): Set<String> = unlockedPowers.toSet()
|
||||||
|
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
|
||||||
|
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific theme is unlocked
|
||||||
|
*/
|
||||||
|
fun isThemeUnlocked(themeId: String): Boolean {
|
||||||
|
return unlockedThemes.contains(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific power is unlocked
|
||||||
|
*/
|
||||||
|
fun isPowerUnlocked(powerId: String): Boolean {
|
||||||
|
return unlockedPowers.contains(powerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Award a badge to the player
|
||||||
|
*/
|
||||||
|
fun awardBadge(badgeId: String): Boolean {
|
||||||
|
val newlyAwarded = unlockedBadges.add(badgeId)
|
||||||
|
if (newlyAwarded) {
|
||||||
|
saveProgress()
|
||||||
|
}
|
||||||
|
return newlyAwarded
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all player progression data
|
||||||
|
*/
|
||||||
|
fun resetProgress() {
|
||||||
|
playerLevel = 1
|
||||||
|
playerXP = 0
|
||||||
|
totalXPEarned = 0
|
||||||
|
|
||||||
|
unlockedThemes.clear()
|
||||||
|
unlockedBlocks.clear()
|
||||||
|
unlockedPowers.clear()
|
||||||
|
unlockedBadges.clear()
|
||||||
|
|
||||||
|
// Add default theme
|
||||||
|
unlockedThemes.add(THEME_CLASSIC)
|
||||||
|
|
||||||
|
saveProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "mintris_progression"
|
||||||
|
private const val KEY_PLAYER_LEVEL = "player_level"
|
||||||
|
private const val KEY_PLAYER_XP = "player_xp"
|
||||||
|
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
|
||||||
|
private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
|
||||||
|
private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks"
|
||||||
|
private const val KEY_UNLOCKED_POWERS = "unlocked_powers"
|
||||||
|
private const val KEY_UNLOCKED_BADGES = "unlocked_badges"
|
||||||
|
|
||||||
|
// XP curve parameters
|
||||||
|
private const val BASE_XP = 5000.0 // Base XP for level 1 (increased from 2500)
|
||||||
|
private const val XP_CURVE_FACTOR = 2.2 // Exponential factor for XP curve (increased from 2.0)
|
||||||
|
|
||||||
|
// XP calculation constants
|
||||||
|
private const val LEVEL_MULTIPLIER = 0.1 // 10% bonus per level
|
||||||
|
private const val XP_PER_LINE = 10L
|
||||||
|
private const val TETRIS_XP_BONUS = 50L
|
||||||
|
private const val PERFECT_CLEAR_XP_BONUS = 200L
|
||||||
|
private const val TIME_XP_PER_MINUTE = 5L
|
||||||
|
|
||||||
|
// Theme IDs with required levels
|
||||||
|
const val THEME_CLASSIC = "theme_classic"
|
||||||
|
const val THEME_NEON = "theme_neon"
|
||||||
|
const val THEME_MONOCHROME = "theme_monochrome"
|
||||||
|
const val THEME_RETRO = "theme_retro"
|
||||||
|
const val THEME_MINIMALIST = "theme_minimalist"
|
||||||
|
const val THEME_GALAXY = "theme_galaxy"
|
||||||
|
|
||||||
|
// Map of themes to required levels
|
||||||
|
val THEME_REQUIRED_LEVELS = mapOf(
|
||||||
|
THEME_CLASSIC to 1,
|
||||||
|
THEME_NEON to 5,
|
||||||
|
THEME_MONOCHROME to 10,
|
||||||
|
THEME_RETRO to 15,
|
||||||
|
THEME_MINIMALIST to 20,
|
||||||
|
THEME_GALAXY to 25
|
||||||
|
)
|
||||||
|
|
||||||
|
// Power IDs
|
||||||
|
const val POWER_FREEZE_TIME = "power_freeze_time"
|
||||||
|
const val POWER_BLOCK_SWAP = "power_block_swap"
|
||||||
|
const val POWER_SAFE_LANDING = "power_safe_landing"
|
||||||
|
const val POWER_PERFECT_CLEAR = "power_perfect_clear"
|
||||||
|
|
||||||
|
// Map of powers to required levels
|
||||||
|
val POWER_REQUIRED_LEVELS = mapOf(
|
||||||
|
POWER_FREEZE_TIME to 8,
|
||||||
|
POWER_BLOCK_SWAP to 12,
|
||||||
|
POWER_SAFE_LANDING to 18,
|
||||||
|
POWER_PERFECT_CLEAR to 30
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the required level for a specific theme
|
||||||
|
*/
|
||||||
|
fun getRequiredLevelForTheme(themeId: String): Int {
|
||||||
|
return THEME_REQUIRED_LEVELS[themeId] ?: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the required level for a specific power
|
||||||
|
*/
|
||||||
|
fun getRequiredLevelForPower(powerId: String): Int {
|
||||||
|
return POWER_REQUIRED_LEVELS[powerId] ?: 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -157,6 +157,27 @@ class StatsManager(context: Context) {
|
||||||
fun getSessionTriples(): Int = sessionTriples
|
fun getSessionTriples(): Int = sessionTriples
|
||||||
fun getSessionTetrises(): Int = sessionTetrises
|
fun getSessionTetrises(): Int = sessionTetrises
|
||||||
|
|
||||||
|
fun resetStats() {
|
||||||
|
// Reset all lifetime stats
|
||||||
|
totalGames = 0
|
||||||
|
totalScore = 0
|
||||||
|
totalLines = 0
|
||||||
|
totalPieces = 0
|
||||||
|
totalTime = 0
|
||||||
|
maxLevel = 0
|
||||||
|
maxScore = 0
|
||||||
|
maxLines = 0
|
||||||
|
|
||||||
|
// Reset line clear stats
|
||||||
|
totalSingles = 0
|
||||||
|
totalDoubles = 0
|
||||||
|
totalTriples = 0
|
||||||
|
totalTetrises = 0
|
||||||
|
|
||||||
|
// Save the reset stats
|
||||||
|
saveStats()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_NAME = "mintris_stats"
|
private const val PREFS_NAME = "mintris_stats"
|
||||||
private const val KEY_TOTAL_GAMES = "total_games"
|
private const val KEY_TOTAL_GAMES = "total_games"
|
||||||
|
|
67
app/src/main/java/com/mintris/ui/LevelBadge.kt
Normal file
67
app/src/main/java/com/mintris/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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
297
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
Normal file
297
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
) {
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// Begin animation sequence
|
||||||
|
xpProgressBar.setXPValues(playerLevel, currentXP, xpForNextLevel)
|
||||||
|
|
||||||
|
// Animate XP gain text entrance
|
||||||
|
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
|
||||||
|
duration = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule animation for the XP bar after text appears
|
||||||
|
postDelayed({
|
||||||
|
xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel)
|
||||||
|
}, 600)
|
||||||
|
|
||||||
|
// If there are new rewards, show them with 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(400)
|
||||||
|
.setStartDelay((i * 150).toLong())
|
||||||
|
.setInterpolator(OvershootInterpolator())
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
}, 2000) // Wait for XP bar animation to finish
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with initial animations
|
||||||
|
AnimatorSet().apply {
|
||||||
|
play(xpTextAnimator)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
// Default background color - will be adjusted based on theme
|
||||||
|
setCardBackgroundColor(Color.BLACK)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set theme color on XP progress bar
|
||||||
|
xpProgressBar.setThemeColor(xpThemeColor)
|
||||||
|
|
||||||
|
// Update card colors for any existing reward cards
|
||||||
|
updateRewardCardColors(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update colors of existing reward cards to match the theme
|
||||||
|
*/
|
||||||
|
private fun updateRewardCardColors(themeId: String) {
|
||||||
|
// Color for card backgrounds based on theme
|
||||||
|
val cardBackgroundColor = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text color for rewards based on theme
|
||||||
|
val rewardTextColor = when (themeId) {
|
||||||
|
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
|
||||||
|
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
|
||||||
|
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
|
||||||
|
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
|
||||||
|
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
|
||||||
|
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
|
||||||
|
else -> Color.WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update each card in the rewards container
|
||||||
|
for (i in 0 until rewardsContainer.childCount) {
|
||||||
|
val card = rewardsContainer.getChildAt(i) as? CardView
|
||||||
|
card?.let {
|
||||||
|
it.setCardBackgroundColor(cardBackgroundColor)
|
||||||
|
|
||||||
|
// Update text color in the card
|
||||||
|
if (it.childCount > 0 && it.getChildAt(0) is TextView) {
|
||||||
|
(it.getChildAt(0) as TextView).setTextColor(rewardTextColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
273
app/src/main/java/com/mintris/ui/ThemeSelector.kt
Normal file
273
app/src/main/java/com/mintris/ui/ThemeSelector.kt
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
package com.mintris.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.GridLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import com.mintris.R
|
||||||
|
import com.mintris.model.PlayerProgressionManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI component for selecting 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
|
||||||
|
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||||
|
|
||||||
|
// Theme cards
|
||||||
|
private val themeCards = mutableMapOf<String, CardView>()
|
||||||
|
|
||||||
|
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
|
||||||
|
themesGrid.removeAllViews()
|
||||||
|
themeCards.clear()
|
||||||
|
|
||||||
|
// Update selected theme
|
||||||
|
selectedTheme = currentTheme
|
||||||
|
|
||||||
|
// Get all possible themes and their details
|
||||||
|
val allThemes = getThemes()
|
||||||
|
|
||||||
|
// Add theme cards to the grid
|
||||||
|
allThemes.forEach { (themeId, themeInfo) ->
|
||||||
|
val isUnlocked = unlockedThemes.contains(themeId)
|
||||||
|
val isSelected = themeId == selectedTheme
|
||||||
|
|
||||||
|
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
||||||
|
themeCards[themeId] = themeCard
|
||||||
|
themesGrid.addView(themeCard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
// Add stroke for selected theme
|
||||||
|
if (isSelected) {
|
||||||
|
setContentPadding(4, 4, 4, 4)
|
||||||
|
// Create a gradient drawable for the border
|
||||||
|
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||||
|
setColor(themeInfo.primaryColor)
|
||||||
|
setStroke(4, Color.WHITE)
|
||||||
|
cornerRadius = 12f
|
||||||
|
}
|
||||||
|
background = gradientDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Only trigger callback if this isn't already the selected theme
|
||||||
|
if (themeId != selectedTheme) {
|
||||||
|
// Update visual state
|
||||||
|
themeCards[selectedTheme]?.cardElevation = 2f
|
||||||
|
card.cardElevation = 8f
|
||||||
|
|
||||||
|
// Update selected theme
|
||||||
|
selectedTheme = themeId
|
||||||
|
|
||||||
|
// Notify listener
|
||||||
|
onThemeSelected?.invoke(themeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
243
app/src/main/java/com/mintris/ui/XPProgressBar.kt
Normal file
243
app/src/main/java/com/mintris/ui/XPProgressBar.kt
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/res/drawable/ic_lock.xml
Normal file
10
app/src/main/res/drawable/ic_lock.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||||
|
</vector>
|
11
app/src/main/res/drawable/lock_overlay.xml
Normal file
11
app/src/main/res/drawable/lock_overlay.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#99000000" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/ic_lock"
|
||||||
|
android:gravity="center" />
|
||||||
|
</layer-list>
|
17
app/src/main/res/drawable/rounded_button.xml
Normal file
17
app/src/main/res/drawable/rounded_button.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="#444444" />
|
||||||
|
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
|
||||||
|
<stroke android:width="1dp" android:color="#666666" />
|
||||||
|
|
||||||
|
<padding
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp"
|
||||||
|
android:bottom="8dp" />
|
||||||
|
|
||||||
|
</shape>
|
|
@ -234,6 +234,17 @@
|
||||||
android:textColor="@color/white" />
|
android:textColor="@color/white" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Player Progression Screen -->
|
||||||
|
<com.mintris.ui.ProgressionScreen
|
||||||
|
android:id="@+id/progressionScreen"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<!-- Settings Menu overlay -->
|
<!-- Settings Menu overlay -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/pauseContainer"
|
android:id="@+id/pauseContainer"
|
||||||
|
@ -244,6 +255,13 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="32dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -251,7 +269,25 @@
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:layout_marginBottom="32dp" />
|
android:layout_marginEnd="16dp" />
|
||||||
|
|
||||||
|
<com.mintris.ui.LevelBadge
|
||||||
|
android:id="@+id/pauseLevelBadge"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/pauseStartButton"
|
android:id="@+id/pauseStartButton"
|
||||||
|
@ -354,28 +390,50 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Theme Selector -->
|
||||||
|
<com.mintris.ui.ThemeSelector
|
||||||
|
android:id="@+id/themeSelector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/settingsButton"
|
android:id="@+id/settingsButton"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="32dp"
|
android:layout_marginTop="16dp"
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/sound_on"
|
android:text="@string/sound_on"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="18sp" />
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/music"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_marginEnd="16dp" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/musicToggle"
|
android:id="@+id/musicToggle"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/toggle_music"
|
android:contentDescription="@string/toggle_music"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:src="@drawable/ic_volume_up"
|
android:src="@drawable/ic_volume_up" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
</LinearLayout>
|
||||||
app:layout_constraintEnd_toStartOf="@+id/pauseStartButton"
|
</LinearLayout>
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
</ScrollView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,15 +1,22 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/black">
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="16dp">
|
android:padding="16dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -143,5 +150,15 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:layout_marginTop="16dp"/>
|
android:layout_marginTop="16dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/resetStatsButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/reset_stats"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:layout_marginTop="16dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
87
app/src/main/res/layout/progression_screen.xml
Normal file
87
app/src/main/res/layout/progression_screen.xml
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/progression_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="LEVEL UP"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/player_level_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Player Level: 1"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<com.mintris.ui.XPProgressBar
|
||||||
|
android:id="@+id/xp_progress_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/xp_gain_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="+0 XP"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textColor="#50C878"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/rewards_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="REWARDS UNLOCKED"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#FFD700"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rewards_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/continue_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="CONTINUE"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
27
app/src/main/res/layout/theme_selector.xml
Normal file
27
app/src/main/res/layout/theme_selector.xml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/available_themes_label"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="AVAILABLE THEMES"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/themes_grid"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:columnCount="3"
|
||||||
|
android:alignmentMode="alignMargins"
|
||||||
|
android:useDefaultMargins="true" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
4
app/src/main/res/values/dimens.xml
Normal file
4
app/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="theme_card_size">80dp</dimen>
|
||||||
|
</resources>
|
|
@ -44,4 +44,6 @@
|
||||||
<string name="doubles">Doubles: %d</string>
|
<string name="doubles">Doubles: %d</string>
|
||||||
<string name="triples">Triples: %d</string>
|
<string name="triples">Triples: %d</string>
|
||||||
<string name="tetrises">Tetrises: %d</string>
|
<string name="tetrises">Tetrises: %d</string>
|
||||||
|
<string name="reset_stats">Reset Stats</string>
|
||||||
|
<string name="music">Music</string>
|
||||||
</resources>
|
</resources>
|
8
tatus
Normal file
8
tatus
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
app/src/main/java/com/mintris/MainActivity.kt
|
||||||
|
app/src/main/java/com/mintris/audio/GameMusic.kt
|
||||||
|
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
|
||||||
|
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
|
||||||
|
app/src/main/java/com/mintris/ui/ThemeSelector.kt
|
||||||
|
app/src/main/java/com/mintris/ui/XPProgressBar.kt
|
||||||
|
app/src/main/res/drawable/rounded_button.xml
|
||||||
|
app/src/main/res/layout/progression_screen.xml
|
Loading…
Add table
Add a link
Reference in a new issue