diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 924b03a..aaa37f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,6 @@ - - + android:theme="@style/Theme.Mintris.NoActionBar"> diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 5390803..e403d6c 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -1,41 +1,40 @@ 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.view.HapticFeedbackConstants +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView 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 @@ -47,8 +46,6 @@ 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 @@ -76,9 +73,6 @@ 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) @@ -88,16 +82,11 @@ class MainActivity : AppCompatActivity() { highScoreManager = HighScoreManager(this) statsManager = StatsManager(this) progressionManager = PlayerProgressionManager(this) - themeSelector = binding.themeSelector - blockSkinSelector = binding.blockSkinSelector // Load and apply theme preference - currentTheme = progressionManager.getSelectedTheme() + currentTheme = loadThemePreference() applyTheme(currentTheme) - // Load and apply block skin preference - gameView.setBlockSkin(progressionManager.getSelectedBlockSkin()) - // Set up game view gameView.setGameBoard(gameBoard) gameView.setHaptics(gameHaptics) @@ -111,7 +100,8 @@ class MainActivity : AppCompatActivity() { } // Set up theme selector - themeSelector.onThemeSelected = { themeId: String -> + val themeSelector = binding.themeSelector + themeSelector.onThemeSelected = { themeId -> // Apply the new theme applyTheme(themeId) @@ -124,18 +114,6 @@ 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 @@ -182,18 +160,18 @@ class MainActivity : AppCompatActivity() { } gameView.onLineClear = { lineCount -> - Log.d(TAG, "Received line clear callback: $lineCount lines") + android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines") // Use enhanced haptic feedback for line clears if (isSoundEnabled) { - Log.d(TAG, "Sound is enabled, triggering haptic feedback") + android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback") try { gameHaptics.vibrateForLineClear(lineCount) - Log.d(TAG, "Haptic feedback triggered successfully") + android.util.Log.d("MainActivity", "Haptic feedback triggered successfully") } catch (e: Exception) { - Log.e(TAG, "Error triggering haptic feedback", e) + android.util.Log.e("MainActivity", "Error triggering haptic feedback", e) } } else { - Log.d(TAG, "Sound is disabled, skipping haptic feedback") + android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback") } // Record line clear in stats statsManager.recordLineClear(lineCount) @@ -446,13 +424,6 @@ class MainActivity : AppCompatActivity() { // Update theme selector updateThemeSelector() - - // Update block skin selector - blockSkinSelector.updateBlockSkins( - progressionManager.getUnlockedBlocks(), - gameView.getCurrentBlockSkin(), - progressionManager.getPlayerLevel() - ) } /** @@ -591,7 +562,7 @@ class MainActivity : AppCompatActivity() { // Save the selected theme currentTheme = themeId - progressionManager.setSelectedTheme(themeId) + saveThemePreference(themeId) // Apply theme to title screen if it's visible if (titleScreen.visibility == View.VISIBLE) { @@ -645,6 +616,22 @@ 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 */ @@ -659,83 +646,4 @@ 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 44e5011..9dc1e87 100644 --- a/app/src/main/java/com/mintris/game/GameHaptics.kt +++ b/app/src/main/java/com/mintris/game/GameHaptics.kt @@ -7,66 +7,15 @@ 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) { - - companion object { - private const val TAG = "GameHaptics" - } - - // Vibrator service - private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + private val 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) { @@ -77,6 +26,40 @@ 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 fe388b5..ce82647 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.hardware.display.DisplayManager +import android.view.WindowManager import android.view.Display +import android.hardware.display.DisplayManager import com.mintris.model.GameBoard import com.mintris.model.Tetromino import com.mintris.model.TetrominoType @@ -33,18 +33,13 @@ 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 - var isPaused = false // Changed from private to public to allow access from MainActivity - private var score = 0 + private var isPaused = false // Callbacks var onNextPieceChanged: (() -> Unit)? = null @@ -133,25 +128,15 @@ class GameView @JvmOverloads constructor( private var lastTapTime = 0L private var lastRotationTime = 0L private var lastMoveTime = 0L - 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 var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop 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 = 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 + private val directionLockThreshold = 1.5f // Threshold for direction lock relative to block size - // Block skin - private var currentBlockSkin: String = "block_skin_1" - private val blockSkinPaints = mutableMapOf() - private enum class Direction { HORIZONTAL, VERTICAL } @@ -175,24 +160,19 @@ class GameView @JvmOverloads constructor( // Connect our callbacks to the GameBoard gameBoard.onPieceMove = { onPieceMove?.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.onPieceLock = { onPieceLock?.invoke() } gameBoard.onLineClear = { lineCount, clearedLines -> - Log.d(TAG, "Received line clear from GameBoard: $lineCount lines") + android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") try { onLineClear?.invoke(lineCount) // Use the lines that were cleared directly linesToPulse.clear() linesToPulse.addAll(clearedLines) - Log.d(TAG, "Found ${linesToPulse.size} lines to pulse") + android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") startPulseAnimation(lineCount) - Log.d(TAG, "Forwarded line clear callback") + android.util.Log.d("GameView", "Forwarded line clear callback") } catch (e: Exception) { - Log.e(TAG, "Error forwarding line clear callback", e) + android.util.Log.e("GameView", "Error forwarding line clear callback", e) } } @@ -201,11 +181,7 @@ class GameView @JvmOverloads constructor( // Set better frame rate using modern APIs val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - displayManager.getDisplay(Display.DEFAULT_DISPLAY) - } else { - displayManager.displays.firstOrNull() - } + val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) display?.let { disp -> val refreshRate = disp.refreshRate // Set game loop interval based on refresh rate, but don't go faster than the base interval @@ -219,64 +195,8 @@ 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 */ @@ -320,8 +240,8 @@ class GameView @JvmOverloads constructor( return } - // Update the game state - gameBoard.update() + // Move the current tetromino down automatically + gameBoard.moveDown() // Update UI with current game state onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) @@ -366,7 +286,7 @@ class GameView @JvmOverloads constructor( val totalHeight = blockSize * verticalBlocks // Log dimensions for debugging - Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") + android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") } override fun onDraw(canvas: Canvas) { @@ -588,68 +508,20 @@ class GameView @JvmOverloads constructor( // Save canvas state before drawing block effects canvas.save() - // Get the current block skin paint - val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!! + // 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) - // 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) + // Draw block + blockPaint.apply { + color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE + 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) // Draw pulse effect if animation is active and this is a pulsing line if (isPulsing && isPulsingLine) { @@ -706,13 +578,6 @@ 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 @@ -747,14 +612,12 @@ class GameView @JvmOverloads constructor( // Check if movement exceeds threshold if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { - // Determine dominant direction with stricter criteria + // Determine dominant direction 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 } } @@ -777,7 +640,7 @@ class GameView @JvmOverloads constructor( } Direction.VERTICAL -> { if (deltaY > blockSize * minMovementThreshold) { - gameBoard.softDrop() + gameBoard.moveDown() lastTouchY = event.y if (currentTime - lastMoveTime >= moveCooldown) { gameHaptics?.vibrateForPieceMove() @@ -797,34 +660,17 @@ class GameView @JvmOverloads constructor( val moveTime = System.currentTimeMillis() - lastTapTime val deltaY = event.y - startY val deltaX = event.x - startX - val currentTime = System.currentTimeMillis() - // 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() - } + // 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() } 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() @@ -884,17 +730,17 @@ class GameView @JvmOverloads constructor( gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onLineClear = { lineCount, clearedLines -> - Log.d(TAG, "Received line clear from GameBoard: $lineCount lines") + android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") try { onLineClear?.invoke(lineCount) // Use the lines that were cleared directly linesToPulse.clear() linesToPulse.addAll(clearedLines) - Log.d(TAG, "Found ${linesToPulse.size} lines to pulse") + android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") startPulseAnimation(lineCount) - Log.d(TAG, "Forwarded line clear callback") + android.util.Log.d("GameView", "Forwarded line clear callback") } catch (e: Exception) { - Log.e(TAG, "Error forwarding line clear callback", e) + android.util.Log.e("GameView", "Error forwarding line clear callback", e) } } @@ -930,7 +776,7 @@ class GameView @JvmOverloads constructor( * Start the pulse animation for line clear */ private fun startPulseAnimation(lineCount: Int) { - Log.d(TAG, "Starting pulse animation for $lineCount lines") + android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines") // Cancel any existing animation pulseAnimator?.cancel() @@ -949,7 +795,7 @@ class GameView @JvmOverloads constructor( pulseAlpha = animation.animatedValue as Float isPulsing = true invalidate() - Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha") + android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha") } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { @@ -957,7 +803,7 @@ class GameView @JvmOverloads constructor( pulseAlpha = 0f linesToPulse.clear() invalidate() - Log.d(TAG, "Pulse animation ended") + android.util.Log.d("GameView", "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 ce26763..5203109 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 android.util.Log +import kotlin.random.Random /** * Represents the game board (grid) and manages game state @@ -9,10 +9,6 @@ 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 } } @@ -38,7 +34,6 @@ 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 @@ -60,13 +55,6 @@ 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() @@ -78,7 +66,7 @@ class GameBoard( private fun spawnNextPiece() { // If bag is empty, refill it with all piece types if (bag.isEmpty()) { - bag.addAll(TetrominoType.entries.toTypedArray()) + bag.addAll(TetrominoType.values()) bag.shuffle() } @@ -126,8 +114,6 @@ 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() @@ -136,15 +122,9 @@ 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") } } } @@ -178,73 +158,29 @@ 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) { - Log.d(TAG, "hardDrop() called but blocked: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking") - return // Prevent multiple hard drops - } + if (isHardDropInProgress || 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() } @@ -332,12 +268,7 @@ class GameBoard( * Lock the current piece in place */ private fun lockPiece() { - 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") + if (isPieceLocking) return // Prevent recursive locking isPieceLocking = true val piece = currentPiece ?: return @@ -363,25 +294,15 @@ 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 locking state + // Reset both states after everything is done isPieceLocking = false - Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress") + isHardDropInProgress = false } /** @@ -405,26 +326,18 @@ 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) { - // Log line clear - Log.d(TAG, "Lines cleared: $shiftAmount") - + android.util.Log.d("GameBoard", "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 { - // Call the line clear callback with the cleared line count + android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines") try { - Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines") - val clearedLines = getLastClearedLines() - onLineClear?.invoke(shiftAmount, clearedLines) - Log.d(TAG, "onLineClear callback completed successfully") + onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared + android.util.Log.d("GameBoard", "onLineClear callback completed successfully") } catch (e: Exception) { - Log.e(TAG, "Error in onLineClear callback", e) + android.util.Log.e("GameBoard", "Error in onLineClear callback", e) } } @@ -669,20 +582,4 @@ 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 bf62a6a..e9260bd 100644 --- a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -20,6 +20,7 @@ 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 @@ -40,21 +41,18 @@ 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") - } } /** @@ -67,6 +65,7 @@ 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() } @@ -180,6 +179,30 @@ 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}" @@ -191,38 +214,11 @@ 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 @@ -231,6 +227,7 @@ 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() @@ -241,6 +238,13 @@ 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 */ @@ -262,14 +266,12 @@ 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() } @@ -280,9 +282,8 @@ 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) @@ -312,6 +313,20 @@ 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 + ) } /** @@ -322,34 +337,9 @@ class PlayerProgressionManager(context: Context) { } /** - * Set the selected block skin + * Get the required level for a specific power */ - 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 + fun getRequiredLevelForPower(powerId: String): Int { + return POWER_REQUIRED_LEVELS[powerId] ?: 1 } } \ 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 deleted file mode 100644 index d042059..0000000 --- a/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt +++ /dev/null @@ -1,308 +0,0 @@ -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 ddea78a..371cb8c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -282,15 +282,13 @@ + android:layout_weight="1"> + android:gravity="center">