diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aaa37f3..924b03a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:theme="@style/Theme.Mintris.NoActionBar" + android:immersive="true" + android:resizeableActivity="false" + android:excludeFromRecents="false"> diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index e403d6c..5390803 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -1,40 +1,41 @@ package com.mintris -import android.animation.ObjectAnimator -import android.animation.ValueAnimator import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import android.os.VibrationEffect import android.os.Vibrator -import android.os.VibratorManager import android.view.View -import android.widget.Button -import android.widget.LinearLayout -import android.widget.TextView +import android.view.HapticFeedbackConstants import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import com.mintris.databinding.ActivityMainBinding import com.mintris.game.GameHaptics import com.mintris.game.GameView -import com.mintris.game.NextPieceView import com.mintris.game.TitleScreen -import android.view.HapticFeedbackConstants import com.mintris.model.GameBoard import com.mintris.audio.GameMusic import com.mintris.model.HighScoreManager import com.mintris.model.PlayerProgressionManager import com.mintris.model.StatsManager import com.mintris.ui.ProgressionScreen +import com.mintris.ui.ThemeSelector +import com.mintris.ui.BlockSkinSelector import java.text.SimpleDateFormat import java.util.* import android.graphics.Color import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import android.graphics.Rect +import android.util.Log +import android.view.KeyEvent class MainActivity : AppCompatActivity() { + companion object { + private const val TAG = "MainActivity" + } + // UI components private lateinit var binding: ActivityMainBinding private lateinit var gameView: GameView @@ -46,6 +47,8 @@ class MainActivity : AppCompatActivity() { private lateinit var statsManager: StatsManager private lateinit var progressionManager: PlayerProgressionManager private lateinit var progressionScreen: ProgressionScreen + private lateinit var themeSelector: ThemeSelector + private lateinit var blockSkinSelector: BlockSkinSelector // Game state private var isSoundEnabled = true @@ -73,6 +76,9 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + // Disable Android back gesture to prevent accidental app exits + disableAndroidBackGesture() + // Initialize game components gameBoard = GameBoard() gameHaptics = GameHaptics(this) @@ -82,11 +88,16 @@ class MainActivity : AppCompatActivity() { highScoreManager = HighScoreManager(this) statsManager = StatsManager(this) progressionManager = PlayerProgressionManager(this) + themeSelector = binding.themeSelector + blockSkinSelector = binding.blockSkinSelector // Load and apply theme preference - currentTheme = loadThemePreference() + currentTheme = progressionManager.getSelectedTheme() applyTheme(currentTheme) + // Load and apply block skin preference + gameView.setBlockSkin(progressionManager.getSelectedBlockSkin()) + // Set up game view gameView.setGameBoard(gameBoard) gameView.setHaptics(gameHaptics) @@ -100,8 +111,7 @@ class MainActivity : AppCompatActivity() { } // Set up theme selector - val themeSelector = binding.themeSelector - themeSelector.onThemeSelected = { themeId -> + themeSelector.onThemeSelected = { themeId: String -> // Apply the new theme applyTheme(themeId) @@ -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 titleScreen.onStartGame = { titleScreen.visibility = View.GONE @@ -160,18 +182,18 @@ class MainActivity : AppCompatActivity() { } 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 if (isSoundEnabled) { - android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback") + Log.d(TAG, "Sound is enabled, triggering haptic feedback") try { gameHaptics.vibrateForLineClear(lineCount) - android.util.Log.d("MainActivity", "Haptic feedback triggered successfully") + Log.d(TAG, "Haptic feedback triggered successfully") } catch (e: Exception) { - android.util.Log.e("MainActivity", "Error triggering haptic feedback", e) + Log.e(TAG, "Error triggering haptic feedback", e) } } 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 statsManager.recordLineClear(lineCount) @@ -424,6 +446,13 @@ class MainActivity : AppCompatActivity() { // Update theme selector updateThemeSelector() + + // Update block skin selector + blockSkinSelector.updateBlockSkins( + progressionManager.getUnlockedBlocks(), + gameView.getCurrentBlockSkin(), + progressionManager.getPlayerLevel() + ) } /** @@ -562,7 +591,7 @@ class MainActivity : AppCompatActivity() { // Save the selected theme currentTheme = themeId - saveThemePreference(themeId) + progressionManager.setSelectedTheme(themeId) // Apply theme to title screen if it's visible if (titleScreen.visibility == View.VISIBLE) { @@ -616,22 +645,6 @@ class MainActivity : AppCompatActivity() { gameView.invalidate() } - /** - * Save the selected theme in preferences - */ - private fun saveThemePreference(themeId: String) { - val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE) - prefs.edit().putString("selected_theme", themeId).apply() - } - - /** - * Load the saved theme preference - */ - private fun loadThemePreference(): String { - val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE) - return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC - } - /** * Get the appropriate color for the current theme */ @@ -646,4 +659,83 @@ class MainActivity : AppCompatActivity() { 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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/GameHaptics.kt b/app/src/main/java/com/mintris/game/GameHaptics.kt index 9dc1e87..44e5011 100644 --- a/app/src/main/java/com/mintris/game/GameHaptics.kt +++ b/app/src/main/java/com/mintris/game/GameHaptics.kt @@ -7,15 +7,66 @@ import android.os.Vibrator import android.os.VibratorManager import android.view.HapticFeedbackConstants import android.view.View +import android.util.Log +/** + * Handles haptic feedback for game events + */ 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 vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") 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) { 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() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index ce82647..fe388b5 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -12,12 +12,12 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet +import android.util.Log import android.view.MotionEvent import android.view.View import android.view.animation.LinearInterpolator -import android.view.WindowManager -import android.view.Display import android.hardware.display.DisplayManager +import android.view.Display import com.mintris.model.GameBoard import com.mintris.model.Tetromino import com.mintris.model.TetrominoType @@ -33,13 +33,18 @@ class GameView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + companion object { + private const val TAG = "GameView" + } + // Game board model private var gameBoard = GameBoard() private var gameHaptics: GameHaptics? = null // Game state 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 var onNextPieceChanged: (() -> Unit)? = null @@ -128,15 +133,25 @@ class GameView @JvmOverloads constructor( private var lastTapTime = 0L private var lastRotationTime = 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 minTapTime = 100L // Minimum time for a tap (in milliseconds) private val rotationCooldown = 150L // Minimum time between rotations (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 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() + private enum class Direction { HORIZONTAL, VERTICAL } @@ -160,19 +175,24 @@ class GameView @JvmOverloads constructor( // Connect our callbacks to the GameBoard 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 -> - android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") + Log.d(TAG, "Received line clear from GameBoard: $lineCount lines") try { onLineClear?.invoke(lineCount) // Use the lines that were cleared directly linesToPulse.clear() 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) - android.util.Log.d("GameView", "Forwarded line clear callback") + Log.d(TAG, "Forwarded line clear callback") } 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 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 -> val refreshRate = disp.refreshRate // 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) { 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 */ @@ -240,8 +320,8 @@ class GameView @JvmOverloads constructor( return } - // Move the current tetromino down automatically - gameBoard.moveDown() + // Update the game state + gameBoard.update() // Update UI with current game state onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) @@ -286,7 +366,7 @@ class GameView @JvmOverloads constructor( val totalHeight = blockSize * verticalBlocks // 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) { @@ -508,20 +588,68 @@ class GameView @JvmOverloads constructor( // Save canvas state before drawing block effects canvas.save() - // 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) + // Get the current block skin paint + val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!! - // Draw block - blockPaint.apply { - color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE - alpha = if (isGhost) 30 else 255 + // Create a clone of the paint to avoid modifying the original + val blockPaint = Paint(paint) + + // 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 if (isPulsing && isPulsingLine) { @@ -578,6 +706,13 @@ class GameView @JvmOverloads constructor( 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) { MotionEvent.ACTION_DOWN -> { // Record start of touch @@ -612,12 +747,14 @@ class GameView @JvmOverloads constructor( // Check if movement exceeds threshold if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { - // Determine dominant direction + // Determine dominant direction with stricter criteria if (absDeltaX > absDeltaY * directionLockThreshold) { lockedDirection = Direction.HORIZONTAL } else if (absDeltaY > absDeltaX * directionLockThreshold) { 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 -> { if (deltaY > blockSize * minMovementThreshold) { - gameBoard.moveDown() + gameBoard.softDrop() lastTouchY = event.y if (currentTime - lastMoveTime >= moveCooldown) { gameHaptics?.vibrateForPieceMove() @@ -660,17 +797,34 @@ class GameView @JvmOverloads constructor( val moveTime = System.currentTimeMillis() - lastTapTime val deltaY = event.y - startY val deltaX = event.x - startX + val currentTime = System.currentTimeMillis() - // If the movement was fast and downward, treat as hard drop - if (moveTime > 0 && deltaY > blockSize * 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) { - gameBoard.hardDrop() - invalidate() + // Check if this might have been a hard drop gesture + val isVerticalSwipe = moveTime > 0 && + deltaY > blockSize * minHardDropDistance && + (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 && abs(deltaY) < maxTapMovement && abs(deltaX) < maxTapMovement) { // Quick tap with minimal movement (rotation) - val currentTime = System.currentTimeMillis() if (currentTime - lastRotationTime >= rotationCooldown) { + Log.d("GameView", "Rotation detected") gameBoard.rotate() lastRotationTime = currentTime invalidate() @@ -730,17 +884,17 @@ class GameView @JvmOverloads constructor( gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() } 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 { onLineClear?.invoke(lineCount) // Use the lines that were cleared directly linesToPulse.clear() 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) - android.util.Log.d("GameView", "Forwarded line clear callback") + Log.d(TAG, "Forwarded line clear callback") } 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 */ 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 pulseAnimator?.cancel() @@ -795,7 +949,7 @@ class GameView @JvmOverloads constructor( pulseAlpha = animation.animatedValue as Float isPulsing = true invalidate() - android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha") + Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha") } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { @@ -803,7 +957,7 @@ class GameView @JvmOverloads constructor( pulseAlpha = 0f linesToPulse.clear() invalidate() - android.util.Log.d("GameView", "Pulse animation ended") + Log.d(TAG, "Pulse animation ended") } }) } diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index 5203109..ce26763 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -1,6 +1,6 @@ package com.mintris.model -import kotlin.random.Random +import android.util.Log /** * Represents the game board (grid) and manages game state @@ -9,6 +9,10 @@ class GameBoard( val width: Int = 10, val height: Int = 20 ) { + companion object { + private const val TAG = "GameBoard" + } + // Board grid to track locked pieces // True = occupied, False = empty private val grid = Array(height) { BooleanArray(width) { false } } @@ -34,6 +38,7 @@ class GameBoard( var isGameOver = false var isHardDropInProgress = false // Make public var isPieceLocking = false // Make public + private var isPlayerSoftDrop = false // Track if the drop is player-initiated // Scoring state private var combo = 0 @@ -55,6 +60,13 @@ class GameBoard( var onNextPieceChanged: (() -> Unit)? = null var onLineClear: ((Int, List) -> Unit)? = null + // Store the last cleared lines + private val lastClearedLines = mutableListOf() + + // Add spawn protection variables + private var pieceSpawnTime = 0L + private val spawnGracePeriod = 250L // Changed from 150ms to 250ms + init { spawnNextPiece() spawnPiece() @@ -66,7 +78,7 @@ class GameBoard( private fun spawnNextPiece() { // If bag is empty, refill it with all piece types if (bag.isEmpty()) { - bag.addAll(TetrominoType.values()) + bag.addAll(TetrominoType.entries.toTypedArray()) bag.shuffle() } @@ -114,6 +126,8 @@ class GameBoard( * Spawns the current tetromino at the top of the board */ fun spawnPiece() { + Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking") + currentPiece = nextPiece spawnNextPiece() @@ -122,9 +136,15 @@ class GameBoard( x = (width - getWidth()) / 2 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) if (!canMove(0, 0)) { isGameOver = true + Log.d(TAG, "spawnPiece() - Game Over condition detected") } } } @@ -158,29 +178,73 @@ class GameBoard( return if (canMove(0, 1)) { 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() true } 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() false } } + /** + * Player-initiated soft drop + */ + fun softDrop() { + isPlayerSoftDrop = true + moveDown() + isPlayerSoftDrop = false + } + /** * Hard drop the current piece */ 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 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 while (canMove(0, 1)) { piece.y++ 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 lockPiece() } @@ -268,7 +332,12 @@ class GameBoard( * Lock the current piece in place */ 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 val piece = currentPiece ?: return @@ -294,15 +363,25 @@ class GameBoard( // Find and clear lines immediately 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 spawnPiece() // Allow holding piece again after locking canHold = true - // Reset both states after everything is done + // Reset locking state isPieceLocking = false - isHardDropInProgress = false + Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress") } /** @@ -326,18 +405,26 @@ class GameBoard( y-- } + // Store the last cleared lines + lastClearedLines.clear() + lastClearedLines.addAll(linesToClear) + // If lines were cleared, calculate score in background and trigger callback 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 val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) mainHandler.post { - android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines") + // Call the line clear callback with the cleared line count try { - onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared - android.util.Log.d("GameBoard", "onLineClear callback completed successfully") + Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines") + val clearedLines = getLastClearedLines() + onLineClear?.invoke(shiftAmount, clearedLines) + Log.d(TAG, "onLineClear callback completed successfully") } 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 */ fun getCombo(): Int = combo + + /** + * Get the list of lines that were most recently cleared + */ + private fun getLastClearedLines(): List { + return lastClearedLines.toList() + } + + /** + * Update the game state (called by game loop) + */ + fun update() { + if (!isGameOver) { + moveDown() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt index e9260bd..bf62a6a 100644 --- a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -20,7 +20,6 @@ class PlayerProgressionManager(context: Context) { // Track unlocked rewards private val unlockedThemes = mutableSetOf() private val unlockedBlocks = mutableSetOf() - private val unlockedPowers = mutableSetOf() private val unlockedBadges = mutableSetOf() // XP gained in the current session @@ -41,18 +40,21 @@ class PlayerProgressionManager(context: Context) { // Load unlocked rewards val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf() val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, setOf()) ?: setOf() - val powersSet = prefs.getStringSet(KEY_UNLOCKED_POWERS, setOf()) ?: setOf() val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf() unlockedThemes.addAll(themesSet) unlockedBlocks.addAll(blocksSet) - unlockedPowers.addAll(powersSet) unlockedBadges.addAll(badgesSet) // Add default theme if nothing is unlocked if (unlockedThemes.isEmpty()) { unlockedThemes.add(THEME_CLASSIC) } + + // Add default block skin if nothing is unlocked + if (unlockedBlocks.isEmpty()) { + unlockedBlocks.add("block_skin_1") + } } /** @@ -65,7 +67,6 @@ class PlayerProgressionManager(context: Context) { .putLong(KEY_TOTAL_XP_EARNED, totalXPEarned) .putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes) .putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks) - .putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers) .putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges) .apply() } @@ -179,30 +180,6 @@ class PlayerProgressionManager(context: Context) { } } - // Check for power unlocks - when (level) { - 8 -> { - if (unlockedPowers.add(POWER_FREEZE_TIME)) { - newRewards.add("Unlocked Freeze Time Power!") - } - } - 12 -> { - if (unlockedPowers.add(POWER_BLOCK_SWAP)) { - newRewards.add("Unlocked Block Swap Power!") - } - } - 18 -> { - if (unlockedPowers.add(POWER_SAFE_LANDING)) { - newRewards.add("Unlocked Safe Landing Power!") - } - } - 30 -> { - if (unlockedPowers.add(POWER_PERFECT_CLEAR)) { - newRewards.add("Unlocked Perfect Clear Power!") - } - } - } - // Check for block skin unlocks if (level % 7 == 0 && level <= 35) { val blockSkin = "block_skin_${level / 7}" @@ -214,11 +191,38 @@ class PlayerProgressionManager(context: Context) { return newRewards } + /** + * Check and unlock any rewards the player should have based on their current level + * This ensures players don't miss unlocks if they level up multiple times at once + */ + private fun checkAllUnlocksForCurrentLevel() { + // Check theme unlocks + if (playerLevel >= 5) unlockedThemes.add(THEME_NEON) + if (playerLevel >= 10) unlockedThemes.add(THEME_MONOCHROME) + if (playerLevel >= 15) unlockedThemes.add(THEME_RETRO) + if (playerLevel >= 20) unlockedThemes.add(THEME_MINIMALIST) + if (playerLevel >= 25) unlockedThemes.add(THEME_GALAXY) + + // Check block skin unlocks + for (i in 1..5) { + val requiredLevel = i * 7 + if (playerLevel >= requiredLevel) { + unlockedBlocks.add("block_skin_$i") + } + } + + // Save any newly unlocked items + saveProgress() + } + /** * Start a new progression session */ fun startNewSession() { sessionXPGained = 0 + + // Ensure all appropriate unlocks are available + checkAllUnlocksForCurrentLevel() } // Getters @@ -227,7 +231,6 @@ class PlayerProgressionManager(context: Context) { fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel) fun getSessionXPGained(): Long = sessionXPGained fun getUnlockedThemes(): Set = unlockedThemes.toSet() - fun getUnlockedPowers(): Set = unlockedPowers.toSet() fun getUnlockedBlocks(): Set = unlockedBlocks.toSet() fun getUnlockedBadges(): Set = unlockedBadges.toSet() @@ -238,13 +241,6 @@ class PlayerProgressionManager(context: Context) { return unlockedThemes.contains(themeId) } - /** - * Check if a specific power is unlocked - */ - fun isPowerUnlocked(powerId: String): Boolean { - return unlockedPowers.contains(powerId) - } - /** * Award a badge to the player */ @@ -266,12 +262,14 @@ class PlayerProgressionManager(context: Context) { unlockedThemes.clear() unlockedBlocks.clear() - unlockedPowers.clear() unlockedBadges.clear() // Add default theme unlockedThemes.add(THEME_CLASSIC) + // Add default block skin + unlockedBlocks.add("block_skin_1") + saveProgress() } @@ -282,8 +280,9 @@ class PlayerProgressionManager(context: Context) { private const val KEY_TOTAL_XP_EARNED = "total_xp_earned" private const val KEY_UNLOCKED_THEMES = "unlocked_themes" private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks" - private const val KEY_UNLOCKED_POWERS = "unlocked_powers" private const val KEY_UNLOCKED_BADGES = "unlocked_badges" + private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin" + private const val KEY_SELECTED_THEME = "selected_theme" // XP curve parameters private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000) @@ -313,20 +312,6 @@ class PlayerProgressionManager(context: Context) { THEME_MINIMALIST to 20, THEME_GALAXY to 25 ) - - // Power IDs - const val POWER_FREEZE_TIME = "power_freeze_time" - const val POWER_BLOCK_SWAP = "power_block_swap" - const val POWER_SAFE_LANDING = "power_safe_landing" - const val POWER_PERFECT_CLEAR = "power_perfect_clear" - - // Map of powers to required levels - val POWER_REQUIRED_LEVELS = mapOf( - POWER_FREEZE_TIME to 8, - POWER_BLOCK_SWAP to 12, - POWER_SAFE_LANDING to 18, - POWER_PERFECT_CLEAR to 30 - ) } /** @@ -337,9 +322,34 @@ class PlayerProgressionManager(context: Context) { } /** - * Get the required level for a specific power + * Set the selected block skin */ - fun getRequiredLevelForPower(powerId: String): Int { - return POWER_REQUIRED_LEVELS[powerId] ?: 1 + fun setSelectedBlockSkin(skinId: String) { + if (unlockedBlocks.contains(skinId)) { + prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).apply() + } + } + + /** + * Get the selected block skin + */ + fun getSelectedBlockSkin(): String { + return prefs.getString(KEY_SELECTED_BLOCK_SKIN, "block_skin_1") ?: "block_skin_1" + } + + /** + * Set the selected theme + */ + fun setSelectedTheme(themeId: String) { + if (unlockedThemes.contains(themeId)) { + prefs.edit().putString(KEY_SELECTED_THEME, themeId).apply() + } + } + + /** + * Get the selected theme + */ + fun getSelectedTheme(): String { + return prefs.getString(KEY_SELECTED_THEME, THEME_CLASSIC) ?: THEME_CLASSIC } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt b/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt new file mode 100644 index 0000000..d042059 --- /dev/null +++ b/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt @@ -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() + + // Player level for determining what should be unlocked + private var playerLevel: Int = 1 + + init { + // Inflate the layout + LayoutInflater.from(context).inflate(R.layout.block_skin_selector, this, true) + + // Get references to views + skinsGrid = findViewById(R.id.skins_grid) + availableSkinsLabel = findViewById(R.id.available_skins_label) + } + + /** + * Update the block skin selector with unlocked skins + */ + fun updateBlockSkins(unlockedSkins: Set, currentSkin: String, playerLevel: Int = 1) { + // Store player level + this.playerLevel = playerLevel + + // Clear existing skin cards + skinsGrid.removeAllViews() + skinCards.clear() + + // Update selected skin + selectedSkin = currentSkin + + // Get all possible skins and their details + val allSkins = getBlockSkins() + + // Add skin cards to the grid + allSkins.forEach { (skinId, skinInfo) -> + val isUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel + val isSelected = skinId == selectedSkin + + val skinCard = createBlockSkinCard(skinId, skinInfo, isUnlocked, isSelected) + skinCards[skinId] = skinCard + skinsGrid.addView(skinCard) + } + } + + /** + * 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 { + return mapOf( + "block_skin_1" to BlockSkinInfo( + displayName = "Classic", + backgroundColor = Color.BLACK, + textColor = Color.WHITE, + unlockLevel = 1 + ), + "block_skin_2" to BlockSkinInfo( + displayName = "Neon", + backgroundColor = Color.parseColor("#0D0221"), + textColor = Color.parseColor("#FF00FF"), + unlockLevel = 7 + ), + "block_skin_3" to BlockSkinInfo( + displayName = "Retro", + backgroundColor = Color.parseColor("#3F2832"), + textColor = Color.parseColor("#FF5A5F"), + unlockLevel = 14 + ), + "block_skin_4" to BlockSkinInfo( + displayName = "Minimalist", + backgroundColor = Color.WHITE, + textColor = Color.BLACK, + unlockLevel = 21 + ), + "block_skin_5" to BlockSkinInfo( + displayName = "Galaxy", + backgroundColor = Color.parseColor("#0B0C10"), + textColor = Color.parseColor("#66FCF1"), + unlockLevel = 28 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 371cb8c..ddea78a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -282,13 +282,15 @@ + android:layout_weight="1" + android:fillViewport="true"> + android:gravity="center" + android:paddingTop="16dp">