Compare commits

...

6 commits

9 changed files with 904 additions and 178 deletions

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<!-- Add permission to handle system gestures if needed on some devices -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -14,7 +16,10 @@
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.Mintris.NoActionBar"> android:theme="@style/Theme.Mintris.NoActionBar"
android:immersive="true"
android:resizeableActivity="false"
android:excludeFromRecents="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View file

@ -1,40 +1,41 @@
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.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
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager
import android.view.View import android.view.View
import android.widget.Button import android.view.HapticFeedbackConstants
import android.widget.LinearLayout
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
import com.mintris.game.NextPieceView
import com.mintris.game.TitleScreen import com.mintris.game.TitleScreen
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 com.mintris.model.PlayerProgressionManager import com.mintris.model.PlayerProgressionManager
import com.mintris.model.StatsManager import com.mintris.model.StatsManager
import com.mintris.ui.ProgressionScreen import com.mintris.ui.ProgressionScreen
import com.mintris.ui.ThemeSelector
import com.mintris.ui.BlockSkinSelector
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import android.graphics.Color import android.graphics.Color
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import android.graphics.Rect
import android.util.Log
import android.view.KeyEvent
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
// UI components // UI components
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var gameView: GameView private lateinit var gameView: GameView
@ -46,6 +47,8 @@ class MainActivity : AppCompatActivity() {
private lateinit var statsManager: StatsManager private lateinit var statsManager: StatsManager
private lateinit var progressionManager: PlayerProgressionManager private lateinit var progressionManager: PlayerProgressionManager
private lateinit var progressionScreen: ProgressionScreen private lateinit var progressionScreen: ProgressionScreen
private lateinit var themeSelector: ThemeSelector
private lateinit var blockSkinSelector: BlockSkinSelector
// Game state // Game state
private var isSoundEnabled = true private var isSoundEnabled = true
@ -73,6 +76,9 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// Disable Android back gesture to prevent accidental app exits
disableAndroidBackGesture()
// Initialize game components // Initialize game components
gameBoard = GameBoard() gameBoard = GameBoard()
gameHaptics = GameHaptics(this) gameHaptics = GameHaptics(this)
@ -82,11 +88,16 @@ class MainActivity : AppCompatActivity() {
highScoreManager = HighScoreManager(this) highScoreManager = HighScoreManager(this)
statsManager = StatsManager(this) statsManager = StatsManager(this)
progressionManager = PlayerProgressionManager(this) progressionManager = PlayerProgressionManager(this)
themeSelector = binding.themeSelector
blockSkinSelector = binding.blockSkinSelector
// Load and apply theme preference // Load and apply theme preference
currentTheme = loadThemePreference() currentTheme = progressionManager.getSelectedTheme()
applyTheme(currentTheme) applyTheme(currentTheme)
// Load and apply block skin preference
gameView.setBlockSkin(progressionManager.getSelectedBlockSkin())
// Set up game view // Set up game view
gameView.setGameBoard(gameBoard) gameView.setGameBoard(gameBoard)
gameView.setHaptics(gameHaptics) gameView.setHaptics(gameHaptics)
@ -100,8 +111,7 @@ class MainActivity : AppCompatActivity() {
} }
// Set up theme selector // Set up theme selector
val themeSelector = binding.themeSelector themeSelector.onThemeSelected = { themeId: String ->
themeSelector.onThemeSelected = { themeId ->
// Apply the new theme // Apply the new theme
applyTheme(themeId) applyTheme(themeId)
@ -114,6 +124,18 @@ class MainActivity : AppCompatActivity() {
} }
} }
// Set up block skin selector
blockSkinSelector.onBlockSkinSelected = { skinId: String ->
// Apply the new block skin
gameView.setBlockSkin(skinId)
// Save the selection
progressionManager.setSelectedBlockSkin(skinId)
// Provide haptic feedback
gameHaptics.vibrateForPieceLock()
}
// Set up title screen // Set up title screen
titleScreen.onStartGame = { titleScreen.onStartGame = {
titleScreen.visibility = View.GONE titleScreen.visibility = View.GONE
@ -160,18 +182,18 @@ class MainActivity : AppCompatActivity() {
} }
gameView.onLineClear = { lineCount -> gameView.onLineClear = { lineCount ->
android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines") Log.d(TAG, "Received line clear callback: $lineCount lines")
// Use enhanced haptic feedback for line clears // Use enhanced haptic feedback for line clears
if (isSoundEnabled) { if (isSoundEnabled) {
android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback") Log.d(TAG, "Sound is enabled, triggering haptic feedback")
try { try {
gameHaptics.vibrateForLineClear(lineCount) gameHaptics.vibrateForLineClear(lineCount)
android.util.Log.d("MainActivity", "Haptic feedback triggered successfully") Log.d(TAG, "Haptic feedback triggered successfully")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("MainActivity", "Error triggering haptic feedback", e) Log.e(TAG, "Error triggering haptic feedback", e)
} }
} else { } else {
android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback") Log.d(TAG, "Sound is disabled, skipping haptic feedback")
} }
// Record line clear in stats // Record line clear in stats
statsManager.recordLineClear(lineCount) statsManager.recordLineClear(lineCount)
@ -424,6 +446,13 @@ class MainActivity : AppCompatActivity() {
// Update theme selector // Update theme selector
updateThemeSelector() updateThemeSelector()
// Update block skin selector
blockSkinSelector.updateBlockSkins(
progressionManager.getUnlockedBlocks(),
gameView.getCurrentBlockSkin(),
progressionManager.getPlayerLevel()
)
} }
/** /**
@ -562,7 +591,7 @@ class MainActivity : AppCompatActivity() {
// Save the selected theme // Save the selected theme
currentTheme = themeId currentTheme = themeId
saveThemePreference(themeId) progressionManager.setSelectedTheme(themeId)
// Apply theme to title screen if it's visible // Apply theme to title screen if it's visible
if (titleScreen.visibility == View.VISIBLE) { if (titleScreen.visibility == View.VISIBLE) {
@ -616,22 +645,6 @@ class MainActivity : AppCompatActivity() {
gameView.invalidate() 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 * Get the appropriate color for the current theme
*/ */
@ -646,4 +659,83 @@ class MainActivity : AppCompatActivity() {
else -> Color.WHITE else -> Color.WHITE
} }
} }
/**
* Disables the Android system back gesture to prevent accidental exits
*/
private fun disableAndroidBackGesture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Set the entire window to be excluded from the system gesture areas
window.decorView.post {
// Create a list of rectangles representing the edges of the screen to exclude from system gestures
val gestureInsets = window.decorView.rootWindowInsets?.systemGestureInsets
if (gestureInsets != null) {
val leftEdge = Rect(0, 0, 50, window.decorView.height)
val rightEdge = Rect(window.decorView.width - 50, 0, window.decorView.width, window.decorView.height)
val bottomEdge = Rect(0, window.decorView.height - 50, window.decorView.width, window.decorView.height)
window.decorView.systemGestureExclusionRects = listOf(leftEdge, rightEdge, bottomEdge)
}
}
}
// Add an on back pressed callback to handle back button/gesture
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackPressedDispatcher.addCallback(this, object : androidx.activity.OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// If we're playing the game, handle it as a pause action instead of exiting
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
gameView.pause()
gameMusic.pause()
showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
} else if (binding.pauseContainer.visibility == View.VISIBLE) {
// If pause menu is showing, handle as a resume
resumeGame()
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
// If game over is showing, go back to title
hideGameOver()
showTitleScreen()
} else if (titleScreen.visibility == View.VISIBLE) {
// If title screen is showing, allow normal back behavior (exit app)
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// For Android 11 (R) to Android 12 (S), use the WindowInsetsController to disable gestures
window.insetsController?.systemBarsBehavior =
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
/**
* Completely block the hardware back button during gameplay
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// If back button is pressed
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Handle back button press as a pause action during gameplay
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
gameView.pause()
gameMusic.pause()
showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
return true // Consume the event
} else if (binding.pauseContainer.visibility == View.VISIBLE) {
// If pause menu is showing, handle as a resume
resumeGame()
return true // Consume the event
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
// If game over is showing, go back to title
hideGameOver()
showTitleScreen()
return true // Consume the event
}
}
return super.onKeyDown(keyCode, event)
}
} }

View file

@ -7,15 +7,66 @@ import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View
import android.util.Log
/**
* Handles haptic feedback for game events
*/
class GameHaptics(private val context: Context) { class GameHaptics(private val context: Context) {
private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
companion object {
private const val TAG = "GameHaptics"
}
// Vibrator service
private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator vibratorManager.defaultVibrator
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
} }
// Vibrate for line clear (more intense for more lines)
fun vibrateForLineClear(lineCount: Int) {
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
// Scale duration and amplitude based on line count
// More lines = longer and stronger vibration
val duration = when(lineCount) {
1 -> 50L // Single line: short vibration
2 -> 80L // Double line: slightly longer
3 -> 120L // Triple line: even longer
4 -> 200L // Tetris: longest vibration
else -> 50L
}
val amplitude = when(lineCount) {
1 -> 80 // Single line: mild vibration (80/255)
2 -> 120 // Double line: medium vibration (120/255)
3 -> 180 // Triple line: strong vibration (180/255)
4 -> 255 // Tetris: maximum vibration (255/255)
else -> 80
}
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
Log.d(TAG, "Vibration triggered successfully")
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
}
} catch (e: Exception) {
Log.e(TAG, "Error triggering vibration", e)
}
}
fun performHapticFeedback(view: View, feedbackType: Int) { fun performHapticFeedback(view: View, feedbackType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@ -26,40 +77,6 @@ class GameHaptics(private val context: Context) {
} }
} }
fun vibrateForLineClear(lineCount: Int) {
android.util.Log.d("GameHaptics", "Attempting to vibrate for $lineCount lines")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val duration = when (lineCount) {
4 -> 200L // Tetris - doubled from 100L
3 -> 160L // Triples - doubled from 80L
2 -> 120L // Doubles - doubled from 60L
1 -> 80L // Singles - doubled from 40L
else -> 0L
}
val amplitude = when (lineCount) {
4 -> 255 // Full amplitude for Tetris
3 -> 230 // 90% amplitude for triples
2 -> 180 // 70% amplitude for doubles
1 -> 128 // 50% amplitude for singles
else -> 0
}
android.util.Log.d("GameHaptics", "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
if (duration > 0 && amplitude > 0) {
try {
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
android.util.Log.d("GameHaptics", "Vibration triggered successfully")
} catch (e: Exception) {
android.util.Log.e("GameHaptics", "Error triggering vibration", e)
}
}
} else {
android.util.Log.w("GameHaptics", "Device does not support vibration effects (Android < 8.0)")
}
}
fun vibrateForPieceLock() { fun vibrateForPieceLock() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE) val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE)

View file

@ -12,12 +12,12 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.view.WindowManager
import android.view.Display
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.view.Display
import com.mintris.model.GameBoard import com.mintris.model.GameBoard
import com.mintris.model.Tetromino import com.mintris.model.Tetromino
import com.mintris.model.TetrominoType import com.mintris.model.TetrominoType
@ -33,13 +33,18 @@ class GameView @JvmOverloads constructor(
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) { ) : View(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "GameView"
}
// Game board model // Game board model
private var gameBoard = GameBoard() private var gameBoard = GameBoard()
private var gameHaptics: GameHaptics? = null private var gameHaptics: GameHaptics? = null
// Game state // Game state
private var isRunning = false private var isRunning = false
private var isPaused = false var isPaused = false // Changed from private to public to allow access from MainActivity
private var score = 0
// Callbacks // Callbacks
var onNextPieceChanged: (() -> Unit)? = null var onNextPieceChanged: (() -> Unit)? = null
@ -128,15 +133,25 @@ class GameView @JvmOverloads constructor(
private var lastTapTime = 0L private var lastTapTime = 0L
private var lastRotationTime = 0L private var lastRotationTime = 0L
private var lastMoveTime = 0L private var lastMoveTime = 0L
private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop private var lastHardDropTime = 0L // Track when the last hard drop occurred
private val hardDropCooldown = 250L // Reduced from 500ms to 250ms
private var touchFreezeUntil = 0L // Time until which touch events should be ignored
private val pieceLockFreezeTime = 300L // Time to freeze touch events after piece locks
private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes
private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels) private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
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 var lockedDirection: Direction? = null // Track the locked movement direction
private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size 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 val directionLockThreshold = 2.5f // Increased from 1.5f to make direction locking more aggressive
private val isStrictDirectionLock = true // Enable strict direction locking to prevent diagonal inputs
private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture
// Block skin
private var currentBlockSkin: String = "block_skin_1"
private val blockSkinPaints = mutableMapOf<String, Paint>()
private enum class Direction { private enum class Direction {
HORIZONTAL, VERTICAL HORIZONTAL, VERTICAL
} }
@ -160,19 +175,24 @@ class GameView @JvmOverloads constructor(
// Connect our callbacks to the GameBoard // Connect our callbacks to the GameBoard
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = {
// Freeze touch events for a brief period after a piece locks
touchFreezeUntil = System.currentTimeMillis() + pieceLockFreezeTime
Log.d(TAG, "Piece locked - freezing touch events until ${touchFreezeUntil}")
onPieceLock?.invoke()
}
gameBoard.onLineClear = { lineCount, clearedLines -> gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") Log.d(TAG, "Received line clear from GameBoard: $lineCount lines")
try { try {
onLineClear?.invoke(lineCount) onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly // Use the lines that were cleared directly
linesToPulse.clear() linesToPulse.clear()
linesToPulse.addAll(clearedLines) linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount) startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback") Log.d(TAG, "Forwarded line clear callback")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e) Log.e(TAG, "Error forwarding line clear callback", e)
} }
} }
@ -181,7 +201,11 @@ class GameView @JvmOverloads constructor(
// Set better frame rate using modern APIs // Set better frame rate using modern APIs
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
displayManager.getDisplay(Display.DEFAULT_DISPLAY)
} else {
displayManager.displays.firstOrNull()
}
display?.let { disp -> display?.let { disp ->
val refreshRate = disp.refreshRate val refreshRate = disp.refreshRate
// Set game loop interval based on refresh rate, but don't go faster than the base interval // Set game loop interval based on refresh rate, but don't go faster than the base interval
@ -195,8 +219,64 @@ class GameView @JvmOverloads constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height))) setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height)))
} }
// Initialize block skin paints
initializeBlockSkinPaints()
} }
/**
* Initialize paints for different block skins
*/
private fun initializeBlockSkinPaints() {
// Classic skin
blockSkinPaints["block_skin_1"] = Paint().apply {
color = Color.WHITE
isAntiAlias = true
}
// Neon skin
blockSkinPaints["block_skin_2"] = Paint().apply {
color = Color.parseColor("#FF00FF")
isAntiAlias = true
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
}
// Retro skin
blockSkinPaints["block_skin_3"] = Paint().apply {
color = Color.parseColor("#FF5A5F")
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 2f
}
// Minimalist skin
blockSkinPaints["block_skin_4"] = Paint().apply {
color = Color.BLACK
isAntiAlias = true
style = Paint.Style.FILL
}
// Galaxy skin
blockSkinPaints["block_skin_5"] = Paint().apply {
color = Color.parseColor("#66FCF1")
isAntiAlias = true
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
}
}
/**
* Set the current block skin
*/
fun setBlockSkin(skinId: String) {
currentBlockSkin = skinId
invalidate()
}
/**
* Get the current block skin
*/
fun getCurrentBlockSkin(): String = currentBlockSkin
/** /**
* Start the game * Start the game
*/ */
@ -240,8 +320,8 @@ class GameView @JvmOverloads constructor(
return return
} }
// Move the current tetromino down automatically // Update the game state
gameBoard.moveDown() gameBoard.update()
// Update UI with current game state // Update UI with current game state
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
@ -286,7 +366,7 @@ class GameView @JvmOverloads constructor(
val totalHeight = blockSize * verticalBlocks val totalHeight = blockSize * verticalBlocks
// Log dimensions for debugging // Log dimensions for debugging
android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
@ -508,20 +588,68 @@ class GameView @JvmOverloads constructor(
// Save canvas state before drawing block effects // Save canvas state before drawing block effects
canvas.save() canvas.save()
// Draw outer glow // Get the current block skin paint
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!!
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
// Draw block // Create a clone of the paint to avoid modifying the original
blockPaint.apply { val blockPaint = Paint(paint)
color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
alpha = if (isGhost) 30 else 255 // Special handling for neon skin
if (currentBlockSkin == "block_skin_2") {
// Stronger outer glow for neon skin
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 0, 255) else Color.parseColor("#FF00FF")
blockGlowPaint.maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER)
canvas.drawRect(left - 4f, top - 4f, right + 4f, bottom + 4f, blockGlowPaint)
// For neon, use semi-translucent fill with strong glowing edges
blockPaint.style = Paint.Style.FILL_AND_STROKE
blockPaint.strokeWidth = 2f
blockPaint.maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
if (isGhost) {
blockPaint.color = Color.argb(30, 255, 0, 255)
blockPaint.alpha = 30
} else {
blockPaint.color = Color.parseColor("#66004D") // Darker magenta fill
blockPaint.alpha = 170 // More opaque to be more visible
}
// Draw block with neon effect
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw a brighter border for better visibility
val borderPaint = Paint().apply {
color = Color.parseColor("#FF00FF")
style = Paint.Style.STROKE
strokeWidth = 3f
alpha = 255
isAntiAlias = true
maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
}
canvas.drawRect(left, top, right, bottom, borderPaint)
// Inner glow for neon blocks - brighter than before
glowPaint.color = if (isGhost) Color.argb(10, 255, 0, 255) else Color.parseColor("#FF00FF")
glowPaint.alpha = if (isGhost) 10 else 100 // More visible inner glow
glowPaint.style = Paint.Style.STROKE
glowPaint.strokeWidth = 2f
glowPaint.maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL)
canvas.drawRect(left + 4f, top + 4f, right - 4f, bottom - 4f, glowPaint)
} else {
// Standard rendering for other skins
// Draw outer glow
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
// Draw block with current skin
blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else blockPaint.color
blockPaint.alpha = if (isGhost) 30 else 255
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
} }
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
// Draw pulse effect if animation is active and this is a pulsing line // Draw pulse effect if animation is active and this is a pulsing line
if (isPulsing && isPulsingLine) { if (isPulsing && isPulsingLine) {
@ -578,6 +706,13 @@ class GameView @JvmOverloads constructor(
return true return true
} }
// Ignore touch events during the freeze period after a piece locks
val currentTime = System.currentTimeMillis()
if (currentTime < touchFreezeUntil) {
Log.d(TAG, "Ignoring touch event - freeze active for ${touchFreezeUntil - currentTime}ms more")
return true
}
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
// Record start of touch // Record start of touch
@ -612,12 +747,14 @@ class GameView @JvmOverloads constructor(
// Check if movement exceeds threshold // Check if movement exceeds threshold
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) {
// Determine dominant direction // Determine dominant direction with stricter criteria
if (absDeltaX > absDeltaY * directionLockThreshold) { if (absDeltaX > absDeltaY * directionLockThreshold) {
lockedDirection = Direction.HORIZONTAL lockedDirection = Direction.HORIZONTAL
} else if (absDeltaY > absDeltaX * directionLockThreshold) { } else if (absDeltaY > absDeltaX * directionLockThreshold) {
lockedDirection = Direction.VERTICAL lockedDirection = Direction.VERTICAL
} }
// If strict direction lock is enabled and we couldn't determine a clear direction, don't set one
// This prevents diagonal movements from being recognized
} }
} }
@ -640,7 +777,7 @@ class GameView @JvmOverloads constructor(
} }
Direction.VERTICAL -> { Direction.VERTICAL -> {
if (deltaY > blockSize * minMovementThreshold) { if (deltaY > blockSize * minMovementThreshold) {
gameBoard.moveDown() gameBoard.softDrop()
lastTouchY = event.y lastTouchY = event.y
if (currentTime - lastMoveTime >= moveCooldown) { if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove() gameHaptics?.vibrateForPieceMove()
@ -660,17 +797,34 @@ class GameView @JvmOverloads constructor(
val moveTime = System.currentTimeMillis() - lastTapTime val moveTime = System.currentTimeMillis() - lastTapTime
val deltaY = event.y - startY val deltaY = event.y - startY
val deltaX = event.x - startX val deltaX = event.x - startX
val currentTime = System.currentTimeMillis()
// If the movement was fast and downward, treat as hard drop // Check if this might have been a hard drop gesture
if (moveTime > 0 && deltaY > blockSize * 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) { val isVerticalSwipe = moveTime > 0 &&
gameBoard.hardDrop() deltaY > blockSize * minHardDropDistance &&
invalidate() (deltaY / moveTime) * 1000 > minSwipeVelocity &&
abs(deltaX) < abs(deltaY) * 0.3f
// Check cooldown separately for better logging
val isCooldownActive = currentTime - lastHardDropTime <= hardDropCooldown
if (isVerticalSwipe) {
if (isCooldownActive) {
// Log when we're blocking a hard drop due to cooldown
Log.d("GameView", "Hard drop blocked by cooldown - time since last: ${currentTime - lastHardDropTime}ms, cooldown: ${hardDropCooldown}ms")
} else {
// Process the hard drop
Log.d("GameView", "Hard drop detected - deltaY: $deltaY, velocity: ${(deltaY / moveTime) * 1000}, ratio: ${abs(deltaX) / abs(deltaY)}")
gameBoard.hardDrop()
lastHardDropTime = currentTime // Update the last hard drop time
invalidate()
}
} else if (moveTime < minTapTime && } else if (moveTime < minTapTime &&
abs(deltaY) < maxTapMovement && abs(deltaY) < maxTapMovement &&
abs(deltaX) < maxTapMovement) { abs(deltaX) < maxTapMovement) {
// Quick tap with minimal movement (rotation) // Quick tap with minimal movement (rotation)
val currentTime = System.currentTimeMillis()
if (currentTime - lastRotationTime >= rotationCooldown) { if (currentTime - lastRotationTime >= rotationCooldown) {
Log.d("GameView", "Rotation detected")
gameBoard.rotate() gameBoard.rotate()
lastRotationTime = currentTime lastRotationTime = currentTime
invalidate() invalidate()
@ -730,17 +884,17 @@ class GameView @JvmOverloads constructor(
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount, clearedLines -> gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") Log.d(TAG, "Received line clear from GameBoard: $lineCount lines")
try { try {
onLineClear?.invoke(lineCount) onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly // Use the lines that were cleared directly
linesToPulse.clear() linesToPulse.clear()
linesToPulse.addAll(clearedLines) linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount) startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback") Log.d(TAG, "Forwarded line clear callback")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e) Log.e(TAG, "Error forwarding line clear callback", e)
} }
} }
@ -776,7 +930,7 @@ class GameView @JvmOverloads constructor(
* Start the pulse animation for line clear * Start the pulse animation for line clear
*/ */
private fun startPulseAnimation(lineCount: Int) { private fun startPulseAnimation(lineCount: Int) {
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines") Log.d(TAG, "Starting pulse animation for $lineCount lines")
// Cancel any existing animation // Cancel any existing animation
pulseAnimator?.cancel() pulseAnimator?.cancel()
@ -795,7 +949,7 @@ class GameView @JvmOverloads constructor(
pulseAlpha = animation.animatedValue as Float pulseAlpha = animation.animatedValue as Float
isPulsing = true isPulsing = true
invalidate() invalidate()
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha") Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha")
} }
addListener(object : android.animation.AnimatorListenerAdapter() { addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) { override fun onAnimationEnd(animation: android.animation.Animator) {
@ -803,7 +957,7 @@ class GameView @JvmOverloads constructor(
pulseAlpha = 0f pulseAlpha = 0f
linesToPulse.clear() linesToPulse.clear()
invalidate() invalidate()
android.util.Log.d("GameView", "Pulse animation ended") Log.d(TAG, "Pulse animation ended")
} }
}) })
} }

View file

@ -1,6 +1,6 @@
package com.mintris.model package com.mintris.model
import kotlin.random.Random import android.util.Log
/** /**
* Represents the game board (grid) and manages game state * Represents the game board (grid) and manages game state
@ -9,6 +9,10 @@ class GameBoard(
val width: Int = 10, val width: Int = 10,
val height: Int = 20 val height: Int = 20
) { ) {
companion object {
private const val TAG = "GameBoard"
}
// Board grid to track locked pieces // Board grid to track locked pieces
// True = occupied, False = empty // True = occupied, False = empty
private val grid = Array(height) { BooleanArray(width) { false } } private val grid = Array(height) { BooleanArray(width) { false } }
@ -34,6 +38,7 @@ class GameBoard(
var isGameOver = false var isGameOver = false
var isHardDropInProgress = false // Make public var isHardDropInProgress = false // Make public
var isPieceLocking = false // Make public var isPieceLocking = false // Make public
private var isPlayerSoftDrop = false // Track if the drop is player-initiated
// Scoring state // Scoring state
private var combo = 0 private var combo = 0
@ -55,6 +60,13 @@ class GameBoard(
var onNextPieceChanged: (() -> Unit)? = null var onNextPieceChanged: (() -> Unit)? = null
var onLineClear: ((Int, List<Int>) -> Unit)? = null var onLineClear: ((Int, List<Int>) -> Unit)? = null
// Store the last cleared lines
private val lastClearedLines = mutableListOf<Int>()
// Add spawn protection variables
private var pieceSpawnTime = 0L
private val spawnGracePeriod = 250L // Changed from 150ms to 250ms
init { init {
spawnNextPiece() spawnNextPiece()
spawnPiece() spawnPiece()
@ -66,7 +78,7 @@ class GameBoard(
private fun spawnNextPiece() { private fun spawnNextPiece() {
// If bag is empty, refill it with all piece types // If bag is empty, refill it with all piece types
if (bag.isEmpty()) { if (bag.isEmpty()) {
bag.addAll(TetrominoType.values()) bag.addAll(TetrominoType.entries.toTypedArray())
bag.shuffle() bag.shuffle()
} }
@ -114,6 +126,8 @@ class GameBoard(
* Spawns the current tetromino at the top of the board * Spawns the current tetromino at the top of the board
*/ */
fun spawnPiece() { fun spawnPiece() {
Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
currentPiece = nextPiece currentPiece = nextPiece
spawnNextPiece() spawnNextPiece()
@ -122,9 +136,15 @@ class GameBoard(
x = (width - getWidth()) / 2 x = (width - getWidth()) / 2
y = 0 y = 0
Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}")
// Set the spawn time for the grace period
pieceSpawnTime = System.currentTimeMillis()
// Check if the piece can be placed (Game Over condition) // Check if the piece can be placed (Game Over condition)
if (!canMove(0, 0)) { if (!canMove(0, 0)) {
isGameOver = true isGameOver = true
Log.d(TAG, "spawnPiece() - Game Over condition detected")
} }
} }
} }
@ -158,29 +178,73 @@ class GameBoard(
return if (canMove(0, 1)) { return if (canMove(0, 1)) {
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0 currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
// Only add soft drop points if it's a player-initiated drop
if (isPlayerSoftDrop) {
score += 1
}
onPieceMove?.invoke() onPieceMove?.invoke()
true true
} else { } else {
// Check if we're within the spawn grace period
val currentTime = System.currentTimeMillis()
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
Log.d(TAG, "moveDown() - not locking piece due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
return false
}
lockPiece() lockPiece()
false false
} }
} }
/**
* Player-initiated soft drop
*/
fun softDrop() {
isPlayerSoftDrop = true
moveDown()
isPlayerSoftDrop = false
}
/** /**
* Hard drop the current piece * Hard drop the current piece
*/ */
fun hardDrop() { fun hardDrop() {
if (isHardDropInProgress || isPieceLocking) return // Prevent multiple hard drops if (isHardDropInProgress || isPieceLocking) {
Log.d(TAG, "hardDrop() called but blocked: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
return // Prevent multiple hard drops
}
// Check if we're within the spawn grace period
val currentTime = System.currentTimeMillis()
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
Log.d(TAG, "hardDrop() - blocked due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
return
}
Log.d(TAG, "hardDrop() started - setting isHardDropInProgress=true")
isHardDropInProgress = true isHardDropInProgress = true
val piece = currentPiece ?: return val piece = currentPiece ?: return
// Count how many cells the piece will drop
var dropDistance = 0
while (canMove(0, dropDistance + 1)) {
dropDistance++
}
Log.d(TAG, "hardDrop() - piece will drop $dropDistance cells, position before: (${piece.x},${piece.y})")
// Move piece down until it can't move anymore // Move piece down until it can't move anymore
while (canMove(0, 1)) { while (canMove(0, 1)) {
piece.y++ piece.y++
onPieceMove?.invoke() onPieceMove?.invoke()
} }
Log.d(TAG, "hardDrop() - piece final position: (${piece.x},${piece.y})")
// Add hard drop points (2 points per cell)
score += dropDistance * 2
// Lock the piece immediately // Lock the piece immediately
lockPiece() lockPiece()
} }
@ -268,7 +332,12 @@ class GameBoard(
* Lock the current piece in place * Lock the current piece in place
*/ */
private fun lockPiece() { private fun lockPiece() {
if (isPieceLocking) return // Prevent recursive locking if (isPieceLocking) {
Log.d(TAG, "lockPiece() called but blocked: isPieceLocking=$isPieceLocking")
return // Prevent recursive locking
}
Log.d(TAG, "lockPiece() started - setting isPieceLocking=true, current isHardDropInProgress=$isHardDropInProgress")
isPieceLocking = true isPieceLocking = true
val piece = currentPiece ?: return val piece = currentPiece ?: return
@ -294,15 +363,25 @@ class GameBoard(
// Find and clear lines immediately // Find and clear lines immediately
findAndClearLines() findAndClearLines()
// IMPORTANT: Reset the hard drop flag before spawning a new piece
// This prevents the immediate hard drop of the next piece
if (isHardDropInProgress) {
Log.d(TAG, "lockPiece() - resetting isHardDropInProgress=false BEFORE spawning new piece")
isHardDropInProgress = false
}
// Log piece position before spawning new piece
Log.d(TAG, "lockPiece() - about to spawn new piece at y=${piece.y}, isHardDropInProgress=$isHardDropInProgress")
// Spawn new piece immediately // Spawn new piece immediately
spawnPiece() spawnPiece()
// Allow holding piece again after locking // Allow holding piece again after locking
canHold = true canHold = true
// Reset both states after everything is done // Reset locking state
isPieceLocking = false isPieceLocking = false
isHardDropInProgress = false Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress")
} }
/** /**
@ -326,18 +405,26 @@ class GameBoard(
y-- y--
} }
// Store the last cleared lines
lastClearedLines.clear()
lastClearedLines.addAll(linesToClear)
// If lines were cleared, calculate score in background and trigger callback // If lines were cleared, calculate score in background and trigger callback
if (shiftAmount > 0) { if (shiftAmount > 0) {
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount") // Log line clear
Log.d(TAG, "Lines cleared: $shiftAmount")
// Trigger line clear callback on main thread with the lines that were cleared // Trigger line clear callback on main thread with the lines that were cleared
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
mainHandler.post { mainHandler.post {
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines") // Call the line clear callback with the cleared line count
try { try {
onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines")
android.util.Log.d("GameBoard", "onLineClear callback completed successfully") val clearedLines = getLastClearedLines()
onLineClear?.invoke(shiftAmount, clearedLines)
Log.d(TAG, "onLineClear callback completed successfully")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameBoard", "Error in onLineClear callback", e) Log.e(TAG, "Error in onLineClear callback", e)
} }
} }
@ -582,4 +669,20 @@ class GameBoard(
* Get the current combo count * Get the current combo count
*/ */
fun getCombo(): Int = combo fun getCombo(): Int = combo
/**
* Get the list of lines that were most recently cleared
*/
private fun getLastClearedLines(): List<Int> {
return lastClearedLines.toList()
}
/**
* Update the game state (called by game loop)
*/
fun update() {
if (!isGameOver) {
moveDown()
}
}
} }

View file

@ -20,7 +20,6 @@ class PlayerProgressionManager(context: Context) {
// Track unlocked rewards // Track unlocked rewards
private val unlockedThemes = mutableSetOf<String>() private val unlockedThemes = mutableSetOf<String>()
private val unlockedBlocks = mutableSetOf<String>() private val unlockedBlocks = mutableSetOf<String>()
private val unlockedPowers = mutableSetOf<String>()
private val unlockedBadges = mutableSetOf<String>() private val unlockedBadges = mutableSetOf<String>()
// XP gained in the current session // XP gained in the current session
@ -41,18 +40,21 @@ class PlayerProgressionManager(context: Context) {
// Load unlocked rewards // Load unlocked rewards
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf() val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, 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() val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf()
unlockedThemes.addAll(themesSet) unlockedThemes.addAll(themesSet)
unlockedBlocks.addAll(blocksSet) unlockedBlocks.addAll(blocksSet)
unlockedPowers.addAll(powersSet)
unlockedBadges.addAll(badgesSet) unlockedBadges.addAll(badgesSet)
// Add default theme if nothing is unlocked // Add default theme if nothing is unlocked
if (unlockedThemes.isEmpty()) { if (unlockedThemes.isEmpty()) {
unlockedThemes.add(THEME_CLASSIC) unlockedThemes.add(THEME_CLASSIC)
} }
// Add default block skin if nothing is unlocked
if (unlockedBlocks.isEmpty()) {
unlockedBlocks.add("block_skin_1")
}
} }
/** /**
@ -65,7 +67,6 @@ class PlayerProgressionManager(context: Context) {
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned) .putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes) .putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks) .putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
.putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers)
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges) .putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
.apply() .apply()
} }
@ -179,30 +180,6 @@ class PlayerProgressionManager(context: Context) {
} }
} }
// Check for power unlocks
when (level) {
8 -> {
if (unlockedPowers.add(POWER_FREEZE_TIME)) {
newRewards.add("Unlocked Freeze Time Power!")
}
}
12 -> {
if (unlockedPowers.add(POWER_BLOCK_SWAP)) {
newRewards.add("Unlocked Block Swap Power!")
}
}
18 -> {
if (unlockedPowers.add(POWER_SAFE_LANDING)) {
newRewards.add("Unlocked Safe Landing Power!")
}
}
30 -> {
if (unlockedPowers.add(POWER_PERFECT_CLEAR)) {
newRewards.add("Unlocked Perfect Clear Power!")
}
}
}
// Check for block skin unlocks // Check for block skin unlocks
if (level % 7 == 0 && level <= 35) { if (level % 7 == 0 && level <= 35) {
val blockSkin = "block_skin_${level / 7}" val blockSkin = "block_skin_${level / 7}"
@ -214,11 +191,38 @@ class PlayerProgressionManager(context: Context) {
return newRewards return newRewards
} }
/**
* Check and unlock any rewards the player should have based on their current level
* This ensures players don't miss unlocks if they level up multiple times at once
*/
private fun checkAllUnlocksForCurrentLevel() {
// Check theme unlocks
if (playerLevel >= 5) unlockedThemes.add(THEME_NEON)
if (playerLevel >= 10) unlockedThemes.add(THEME_MONOCHROME)
if (playerLevel >= 15) unlockedThemes.add(THEME_RETRO)
if (playerLevel >= 20) unlockedThemes.add(THEME_MINIMALIST)
if (playerLevel >= 25) unlockedThemes.add(THEME_GALAXY)
// Check block skin unlocks
for (i in 1..5) {
val requiredLevel = i * 7
if (playerLevel >= requiredLevel) {
unlockedBlocks.add("block_skin_$i")
}
}
// Save any newly unlocked items
saveProgress()
}
/** /**
* Start a new progression session * Start a new progression session
*/ */
fun startNewSession() { fun startNewSession() {
sessionXPGained = 0 sessionXPGained = 0
// Ensure all appropriate unlocks are available
checkAllUnlocksForCurrentLevel()
} }
// Getters // Getters
@ -227,7 +231,6 @@ class PlayerProgressionManager(context: Context) {
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel) fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
fun getSessionXPGained(): Long = sessionXPGained fun getSessionXPGained(): Long = sessionXPGained
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet() fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
fun getUnlockedPowers(): Set<String> = unlockedPowers.toSet()
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet() fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet() fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
@ -238,13 +241,6 @@ class PlayerProgressionManager(context: Context) {
return unlockedThemes.contains(themeId) 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 * Award a badge to the player
*/ */
@ -266,12 +262,14 @@ class PlayerProgressionManager(context: Context) {
unlockedThemes.clear() unlockedThemes.clear()
unlockedBlocks.clear() unlockedBlocks.clear()
unlockedPowers.clear()
unlockedBadges.clear() unlockedBadges.clear()
// Add default theme // Add default theme
unlockedThemes.add(THEME_CLASSIC) unlockedThemes.add(THEME_CLASSIC)
// Add default block skin
unlockedBlocks.add("block_skin_1")
saveProgress() saveProgress()
} }
@ -282,8 +280,9 @@ class PlayerProgressionManager(context: Context) {
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned" private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
private const val KEY_UNLOCKED_THEMES = "unlocked_themes" private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks" private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks"
private const val KEY_UNLOCKED_POWERS = "unlocked_powers"
private const val KEY_UNLOCKED_BADGES = "unlocked_badges" private const val KEY_UNLOCKED_BADGES = "unlocked_badges"
private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin"
private const val KEY_SELECTED_THEME = "selected_theme"
// XP curve parameters // XP curve parameters
private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000) private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000)
@ -313,20 +312,6 @@ class PlayerProgressionManager(context: Context) {
THEME_MINIMALIST to 20, THEME_MINIMALIST to 20,
THEME_GALAXY to 25 THEME_GALAXY to 25
) )
// Power IDs
const val POWER_FREEZE_TIME = "power_freeze_time"
const val POWER_BLOCK_SWAP = "power_block_swap"
const val POWER_SAFE_LANDING = "power_safe_landing"
const val POWER_PERFECT_CLEAR = "power_perfect_clear"
// Map of powers to required levels
val POWER_REQUIRED_LEVELS = mapOf(
POWER_FREEZE_TIME to 8,
POWER_BLOCK_SWAP to 12,
POWER_SAFE_LANDING to 18,
POWER_PERFECT_CLEAR to 30
)
} }
/** /**
@ -337,9 +322,34 @@ class PlayerProgressionManager(context: Context) {
} }
/** /**
* Get the required level for a specific power * Set the selected block skin
*/ */
fun getRequiredLevelForPower(powerId: String): Int { fun setSelectedBlockSkin(skinId: String) {
return POWER_REQUIRED_LEVELS[powerId] ?: 1 if (unlockedBlocks.contains(skinId)) {
prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).apply()
}
}
/**
* Get the selected block skin
*/
fun getSelectedBlockSkin(): String {
return prefs.getString(KEY_SELECTED_BLOCK_SKIN, "block_skin_1") ?: "block_skin_1"
}
/**
* Set the selected theme
*/
fun setSelectedTheme(themeId: String) {
if (unlockedThemes.contains(themeId)) {
prefs.edit().putString(KEY_SELECTED_THEME, themeId).apply()
}
}
/**
* Get the selected theme
*/
fun getSelectedTheme(): String {
return prefs.getString(KEY_SELECTED_THEME, THEME_CLASSIC) ?: THEME_CLASSIC
} }
} }

View file

@ -0,0 +1,308 @@
package com.mintris.ui
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
/**
* UI component for selecting block skins
*/
class BlockSkinSelector @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val skinsGrid: GridLayout
private val availableSkinsLabel: TextView
// Callback when a block skin is selected
var onBlockSkinSelected: ((String) -> Unit)? = null
// Currently selected block skin
private var selectedSkin: String = "block_skin_1"
// Block skin cards
private val skinCards = mutableMapOf<String, CardView>()
// Player level for determining what should be unlocked
private var playerLevel: Int = 1
init {
// Inflate the layout
LayoutInflater.from(context).inflate(R.layout.block_skin_selector, this, true)
// Get references to views
skinsGrid = findViewById(R.id.skins_grid)
availableSkinsLabel = findViewById(R.id.available_skins_label)
}
/**
* Update the block skin selector with unlocked skins
*/
fun updateBlockSkins(unlockedSkins: Set<String>, currentSkin: String, playerLevel: Int = 1) {
// Store player level
this.playerLevel = playerLevel
// Clear existing skin cards
skinsGrid.removeAllViews()
skinCards.clear()
// Update selected skin
selectedSkin = currentSkin
// Get all possible skins and their details
val allSkins = getBlockSkins()
// Add skin cards to the grid
allSkins.forEach { (skinId, skinInfo) ->
val isUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
val isSelected = skinId == selectedSkin
val skinCard = createBlockSkinCard(skinId, skinInfo, isUnlocked, isSelected)
skinCards[skinId] = skinCard
skinsGrid.addView(skinCard)
}
}
/**
* Set the selected skin with a visual effect
*/
fun setSelectedSkin(skinId: String) {
if (skinId == selectedSkin) return
// Update previously selected card
skinCards[selectedSkin]?.let { prevCard ->
prevCard.cardElevation = 2f
// Reset any special styling
prevCard.background = null
prevCard.setCardBackgroundColor(getBlockSkins()[selectedSkin]?.backgroundColor ?: Color.BLACK)
}
// Update visual state of newly selected card
skinCards[skinId]?.let { card ->
card.cardElevation = 12f
// Flash animation for selection feedback
val flashColor = Color.WHITE
val originalColor = getBlockSkins()[skinId]?.backgroundColor ?: Color.BLACK
// Create animator for flash effect
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
flashAnimator.duration = 300 // 300ms
flashAnimator.addUpdateListener { animator ->
val color = animator.animatedValue as Int
card.setCardBackgroundColor(color)
}
flashAnimator.start()
// Add special border to selected card
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
setColor(originalColor)
setStroke(6, Color.WHITE) // Thicker border
cornerRadius = 12f
}
card.background = gradientDrawable
}
// Update selected skin
selectedSkin = skinId
}
/**
* Create a card for a block skin
*/
private fun createBlockSkinCard(
skinId: String,
skinInfo: BlockSkinInfo,
isUnlocked: Boolean,
isSelected: Boolean
): CardView {
// Create the card
val card = CardView(context).apply {
id = View.generateViewId()
radius = 12f
cardElevation = if (isSelected) 8f else 2f
useCompatPadding = true
// Set card background color based on skin
setCardBackgroundColor(skinInfo.backgroundColor)
// Add more noticeable visual indicator for selected skin
if (isSelected) {
setContentPadding(4, 4, 4, 4)
// Create a gradient drawable for the border
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
setColor(skinInfo.backgroundColor)
setStroke(6, Color.WHITE) // Thicker border
cornerRadius = 12f
}
background = gradientDrawable
// Add glow effect via elevation
cardElevation = 12f
}
// Set card dimensions
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
layoutParams = GridLayout.LayoutParams().apply {
width = cardSize
height = cardSize
columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
setMargins(8, 8, 8, 8)
}
// Apply locked/selected state visuals
alpha = if (isUnlocked) 1.0f else 0.5f
}
// Create block skin content container
val container = FrameLayout(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
// Create the block skin preview
val blockSkinPreview = View(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// Set the background color
setBackgroundColor(skinInfo.backgroundColor)
}
// Add a label with the skin name
val skinLabel = TextView(context).apply {
text = skinInfo.displayName
setTextColor(skinInfo.textColor)
textSize = 14f
textAlignment = View.TEXT_ALIGNMENT_CENTER
// Position at the bottom of the card
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
setMargins(4, 4, 4, 8)
}
}
// Add level requirement for locked skins
val levelRequirement = TextView(context).apply {
text = "Level ${skinInfo.unlockLevel}"
setTextColor(Color.WHITE)
textSize = 12f
textAlignment = View.TEXT_ALIGNMENT_CENTER
visibility = if (isUnlocked) View.GONE else View.VISIBLE
// Position at the center of the card
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = android.view.Gravity.CENTER
}
// Make text bold and more visible for better readability
typeface = android.graphics.Typeface.DEFAULT_BOLD
setShadowLayer(3f, 1f, 1f, Color.BLACK)
}
// Add a lock icon if the skin is locked
val lockOverlay = View(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// Add lock icon or visual indicator
setBackgroundResource(R.drawable.lock_overlay)
visibility = if (isUnlocked) View.GONE else View.VISIBLE
}
// Add all elements to container
container.addView(blockSkinPreview)
container.addView(skinLabel)
container.addView(lockOverlay)
container.addView(levelRequirement)
// Add container to card
card.addView(container)
// Set up click listener only for unlocked skins
if (isUnlocked) {
card.setOnClickListener {
// Only trigger callback if this isn't already the selected skin
if (skinId != selectedSkin) {
// Update UI for selection
setSelectedSkin(skinId)
// Notify listener
onBlockSkinSelected?.invoke(skinId)
}
}
}
return card
}
/**
* Data class for block skin information
*/
data class BlockSkinInfo(
val displayName: String,
val backgroundColor: Int,
val textColor: Int,
val unlockLevel: Int
)
/**
* Get all available block skins with their details
*/
private fun getBlockSkins(): Map<String, BlockSkinInfo> {
return mapOf(
"block_skin_1" to BlockSkinInfo(
displayName = "Classic",
backgroundColor = Color.BLACK,
textColor = Color.WHITE,
unlockLevel = 1
),
"block_skin_2" to BlockSkinInfo(
displayName = "Neon",
backgroundColor = Color.parseColor("#0D0221"),
textColor = Color.parseColor("#FF00FF"),
unlockLevel = 7
),
"block_skin_3" to BlockSkinInfo(
displayName = "Retro",
backgroundColor = Color.parseColor("#3F2832"),
textColor = Color.parseColor("#FF5A5F"),
unlockLevel = 14
),
"block_skin_4" to BlockSkinInfo(
displayName = "Minimalist",
backgroundColor = Color.WHITE,
textColor = Color.BLACK,
unlockLevel = 21
),
"block_skin_5" to BlockSkinInfo(
displayName = "Galaxy",
backgroundColor = Color.parseColor("#0B0C10"),
textColor = Color.parseColor("#66FCF1"),
unlockLevel = 28
)
)
}
}

View file

@ -282,13 +282,15 @@
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"> android:layout_weight="1"
android:fillViewport="true">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:gravity="center"
android:paddingTop="16dp">
<Button <Button
android:id="@+id/pauseStartButton" android:id="@+id/pauseStartButton"
@ -399,6 +401,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<!-- Block Skin Selector -->
<com.mintris.ui.BlockSkinSelector
android:id="@+id/blockSkinSelector"
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"

View 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_skins_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="BLOCK SKINS"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="16dp" />
<GridLayout
android:id="@+id/skins_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:alignmentMode="alignMargins"
android:useDefaultMargins="true" />
</LinearLayout>