diff --git a/README.md b/README.md index 3e4bad7..eb42044 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ Pixelmint Drop features a comprehensive scoring system designed to reward skillf - **Swipe down quickly:** Hard drop (instant placement). - **Swipe down slowly:** Soft drop (faster downward movement). - **Single tap:** Rotate piece. -- **Swipe up:** Hold piece. ### Visual Effects - Silky-smooth piece movement animations. @@ -71,8 +70,6 @@ Pixelmint Drop features a comprehensive scoring system designed to reward skillf - Subtle block glow effects. - Clean grid lines for better visibility. - Engaging animated title screen featuring falling pieces. -- Multiple theme options with ability to change manually or enable Random Mode (unlocked when 2+ themes are available). -- In Random Mode, themes change automatically every 10 line clears (1 level). ## Technical Details diff --git a/app/build.gradle b/app/build.gradle index 0466731..bcde979 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.pixelmintdrop" minSdk 30 targetSdk 35 - versionCode 2 - versionName "0.1" + versionCode 1 + versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/pixelmintdrop/MainActivity.kt b/app/src/main/java/com/pixelmintdrop/MainActivity.kt index 5701d19..da12577 100644 --- a/app/src/main/java/com/pixelmintdrop/MainActivity.kt +++ b/app/src/main/java/com/pixelmintdrop/MainActivity.kt @@ -357,12 +357,6 @@ class MainActivity : AppCompatActivity(), binding.gameControlsContainer.visibility = View.GONE titleScreen.visibility = View.VISIBLE - // Hide landscape control panels if in landscape mode and title screen is visible - if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { - binding.leftControlsPanel?.visibility = View.GONE - binding.rightControlsPanel?.visibility = View.GONE - } - // Set up pause button to show settings menu binding.pauseButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) @@ -1138,20 +1132,6 @@ class MainActivity : AppCompatActivity(), // Check for already connected gamepads checkGamepadConnections() - // Update visibility of control panels in landscape orientation based on title screen visibility - if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { - if (titleScreen.visibility == View.VISIBLE) { - // Hide control panels when title screen is visible - binding.leftControlsPanel?.visibility = View.GONE - binding.rightControlsPanel?.visibility = View.GONE - } else if (gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE - && binding.pauseContainer.visibility == View.GONE) { - // Show control panels when game is active - binding.leftControlsPanel?.visibility = View.VISIBLE - binding.rightControlsPanel?.visibility = View.VISIBLE - } - } - // If we're on the title screen, don't auto-resume the game if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) { resumeGame() diff --git a/app/src/main/java/com/pixelmintdrop/game/GameView.kt b/app/src/main/java/com/pixelmintdrop/game/GameView.kt index 4eb2e50..305e2f8 100644 --- a/app/src/main/java/com/pixelmintdrop/game/GameView.kt +++ b/app/src/main/java/com/pixelmintdrop/game/GameView.kt @@ -24,7 +24,6 @@ import com.pixelmintdrop.model.GameBoard import com.pixelmintdrop.model.Tetromino import com.pixelmintdrop.model.TetrominoType import kotlin.math.abs -import kotlin.math.min /** * GameView that renders the Tetris game and handles touch input @@ -59,11 +58,11 @@ class GameView @JvmOverloads constructor( private val borderGlowPaint = Paint().apply { color = Color.WHITE - alpha = 90 // Increased from 60 + alpha = 60 isAntiAlias = true style = Paint.Style.STROKE strokeWidth = 2f - maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) // Increased from 8f + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) } private val ghostBlockPaint = Paint().apply { @@ -142,7 +141,7 @@ class GameView @JvmOverloads constructor( private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes private val maxTapMovement = 30f // Increased from 20f to 30f for more lenient tap detection private val minTapTime = 100L // Minimum time for a tap (in milliseconds) - private val rotationCooldown = 100L // Reduced from 150L to allow faster rotation taps + private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) private val doubleTapTimeout = 400L // Increased from 300ms to 400ms for more lenient double tap detection private var lastTapX = 0f // X coordinate of last tap @@ -443,32 +442,28 @@ class GameView @JvmOverloads constructor( * Calculate dimensions for the board and blocks based on view size */ private fun calculateDimensions(width: Int, height: Int) { + // Calculate block size based on available space val horizontalBlocks = gameBoard.width val verticalBlocks = gameBoard.height - // Go back to zero padding for flush border - val borderPadding = 0f - - // Calculate available drawing area (entire view) - val availableWidth = width.toFloat() - (borderPadding * 2) - val availableHeight = height.toFloat() - (borderPadding * 2) - - // Calculate potential block sizes based on fitting width and height separately - val blockSizeBasedOnWidth = availableWidth / horizontalBlocks - val blockSizeBasedOnHeight = availableHeight / verticalBlocks - - // Use the smaller block size to ensure the entire board fits within the padded area - blockSize = minOf(blockSizeBasedOnWidth, blockSizeBasedOnHeight) - - // Calculate the final dimensions of the board using the determined block size - val finalBoardWidth = blockSize * horizontalBlocks - val finalBoardHeight = blockSize * verticalBlocks - - // Center the final board area within the entire view - boardLeft = (width.toFloat() - finalBoardWidth) / 2 - boardTop = (height.toFloat() - finalBoardHeight) / 2 - + + // Account for all glow effects and borders + val borderPadding = 16f // Padding for border glow effects + + // Calculate block size to fit the height exactly, accounting for all padding + blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks + + // Calculate total board width + val totalBoardWidth = blockSize * horizontalBlocks + + // Center horizontally + boardLeft = (width - totalBoardWidth) / 2 + boardTop = borderPadding // Start with border padding from top + + // Calculate the total height needed for the board + val totalHeight = blockSize * verticalBlocks + // Log dimensions for debugging - Log.d(TAG, "Board dimensions (Small Padding, Centered): view($width, $height), block($blockSize), board($finalBoardWidth, $finalBoardHeight), pos($boardLeft, $boardTop)") + Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") } override fun onDraw(canvas: Canvas) { @@ -637,19 +632,12 @@ class GameView @JvmOverloads constructor( val rect = RectF(left, top, right, bottom) - // Draw base border with updated glow properties from the paint object - canvas.drawRect(rect, borderGlowPaint) - - // Draw a sharp inner line for a flush appearance - val sharpBorderPaint = Paint().apply { - color = Color.WHITE - alpha = 90 // Slightly less intense than glow - isAntiAlias = true - style = Paint.Style.STROKE - strokeWidth = 1f // Very thin line - maskFilter = null // No blur + // Draw base border with increased glow + borderGlowPaint.apply { + alpha = 80 // Increased from 60 + maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f } - canvas.drawRect(rect, sharpBorderPaint) + canvas.drawRect(rect, borderGlowPaint) // Draw pulsing border if animation is active if (isPulsing) { diff --git a/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt b/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt index 3fece82..dee6d08 100644 --- a/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt +++ b/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt @@ -238,57 +238,13 @@ class TitleScreen @JvmOverloads constructor( // Remove tetrominos that fell off the screen tetrominos.removeAll(tetrominosToRemove) - // Adjust high scores position based on title position - val highScoreY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { - height * 0.45f // In landscape, position high scores below title with more space - } else { - height * 0.5f // In portrait, keep in middle of screen - } - - // Position title based on orientation for optimal positioning - val titleY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { - // In landscape mode, position halfway between top and high scores - highScoreY / 2 - } else { - // In portrait mode, position halfway between top and high scores - highScoreY / 2 - } - - // Measure each word without spaces - val pixelWidth = titlePaint.measureText("pixel") - val mintWidth = titlePaint.measureText("mint") - val dropWidth = titlePaint.measureText("drop") - - // Define even space between words - val wordSpacing = 40f // Consistent spacing between words - - // Calculate total width including spacing - val totalWidth = pixelWidth + mintWidth + dropWidth + (wordSpacing * 2) - - // Start position for first word to center the entire title - val startX = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { - width / 2f - totalWidth / 2 // Center in landscape - } else { - width / 2f - totalWidth / 2 + 100f // Add offset to the right in portrait - } - - // Draw "pixel" in theme color - canvas.drawText("pixel", startX, titleY, titlePaint) - - // Save the original color - val originalColor = titlePaint.color - - // Draw "mint" in mint color (#3EB489) with consistent spacing - titlePaint.color = Color.parseColor("#3EB489") - canvas.drawText("mint", startX + pixelWidth + wordSpacing, titleY, titlePaint) - - // Restore the original color and draw "drop" with consistent spacing - titlePaint.color = originalColor - canvas.drawText("drop", startX + pixelWidth + mintWidth + (wordSpacing * 2), titleY, titlePaint) + // Draw title + val titleY = height * 0.4f + canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint) // Draw high scores using pre-allocated manager val highScores: List = highScoreManager.getHighScores() - + val highScoreY = height * 0.5f var lastHighScoreY = highScoreY if (highScores.isNotEmpty()) { // Calculate the starting X position to center the entire block of scores diff --git a/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt index 1802863..bca6233 100644 --- a/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt +++ b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt @@ -148,26 +148,17 @@ class GameBoard( currentPiece = nextPiece spawnNextPiece() - // Center the piece horizontally + // Center the piece horizontally and spawn one unit higher currentPiece?.apply { x = (width - getWidth()) / 2 - // Spawn piece at the top row (y=0) - y = 0 - + y = -1 // Spawn one unit above the top of the screen + Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}") - // Allow holding again if a new piece spawns naturally - if (!isPieceLocking) { // Avoid resetting hold during lockPiece sequence - canHold = true - } - - // Reset soft drop flag - isPlayerSoftDrop = false - - // Record spawn time for grace period + // Set the spawn time for the grace period pieceSpawnTime = System.currentTimeMillis() - // Check if the piece can be placed at y=0 (Game Over condition) + // Check if the piece can be placed (Game Over condition) if (!canMove(0, 0)) { isGameOver = true Log.d(TAG, "spawnPiece() - Game Over condition detected") @@ -279,58 +270,42 @@ class GameBoard( * Rotate the current piece clockwise */ fun rotate() { - currentPiece?.let { piece -> - // Save current rotation and position - val originalRotationIndex = piece.getRotationIndex() // Use getter - val originalX = piece.x - val originalY = piece.y - + currentPiece?.let { + // Save current rotation + val originalX = it.x + val originalY = it.y + // Try to rotate - piece.rotateClockwise() - Log.d(TAG, "Attempting rotate CW. Original pos: ($originalX, $originalY)") - - // Check if the new rotation is valid at the original position - if (canMove(0, 0)) { - // Rotation is valid without kicks - Log.d(TAG, "Rotate CW successful without kick at: (${piece.x}, ${piece.y})") - onPieceMove?.invoke() - return@let // Exit the let block - } - + it.rotateClockwise() + // Wall kick logic - try to move the piece if rotation causes collision - // Define standard kicks (adjust based on SRS rules if needed) - // Order matters: check common kicks first - val kicks = listOf( - Pair(-1, 0), Pair(1, 0), // Basic wall kicks L/R - Pair(-1, -1), Pair(1, -1), // Kicks slighty up (sometimes needed) - Pair(0, -1), // Floor kick - Pair(-2, 0), Pair(2, 0) // Kicks for I piece - // Add more complex SRS kicks if necessary - ) - - var kickApplied = false - for ((kickX, kickY) in kicks) { - // Check canMove relative to the ORIGINAL position + kick offset - // Temporarily set position for the check - piece.x = originalX + kickX - piece.y = originalY + kickY - if (canMove(0, 0)) { // Check validity at the kicked position - Log.d(TAG, "Rotate CW kick applied: ($kickX, $kickY), new pos: (${piece.x}, ${piece.y})") - kickApplied = true - onPieceMove?.invoke() - break // Found a valid kick, stop checking + if (!canMove(0, 0)) { + // Try to move left + if (canMove(-1, 0)) { + it.x-- + } + // Try to move right + else if (canMove(1, 0)) { + it.x++ + } + // Try to move 2 spaces (for I piece) + else if (canMove(-2, 0)) { + it.x -= 2 + } + else if (canMove(2, 0)) { + it.x += 2 + } + // Try to move up for floor kicks + else if (canMove(0, -1)) { + it.y-- + } + // Revert if can't find a valid position + else { + it.rotateCounterClockwise() + it.x = originalX + it.y = originalY } } - - // Revert if no kick worked - if (!kickApplied) { - Log.d(TAG, "Rotate CW failed - reverting rotation and position") - piece.setRotationIndex(originalRotationIndex) // Use setter - piece.x = originalX // Revert position - piece.y = originalY - // Do not call onPieceMove if rotation failed - } - // No need for onPieceMove here as it's called when kick succeeds or initial rotation is fine } } @@ -338,58 +313,42 @@ class GameBoard( * Rotate the current piece counterclockwise */ fun rotateCounterClockwise() { - currentPiece?.let { piece -> - // Save current rotation and position - val originalRotationIndex = piece.getRotationIndex() // Use getter - val originalX = piece.x - val originalY = piece.y - + currentPiece?.let { + // Save current rotation + val originalX = it.x + val originalY = it.y + // Try to rotate - piece.rotateCounterClockwise() - Log.d(TAG, "Attempting rotate CCW. Original pos: ($originalX, $originalY)") - - // Check if the new rotation is valid at the original position - if (canMove(0, 0)) { - // Rotation is valid without kicks - Log.d(TAG, "Rotate CCW successful without kick at: (${piece.x}, ${piece.y})") - onPieceMove?.invoke() - return@let // Exit the let block - } - + it.rotateCounterClockwise() + // Wall kick logic - try to move the piece if rotation causes collision - // Define standard kicks (adjust based on SRS rules if needed) - // Order matters: check common kicks first - val kicks = listOf( - Pair(1, 0), Pair(-1, 0), // Basic wall kicks R/L (opposite order might be better for CCW?) - Pair(1, -1), Pair(-1, -1), // Kicks slighty up - Pair(0, -1), // Floor kick - Pair(2, 0), Pair(-2, 0) // Kicks for I piece - // Add more complex SRS kicks if necessary - ) - - var kickApplied = false - for ((kickX, kickY) in kicks) { - // Check canMove relative to the ORIGINAL position + kick offset - // Temporarily set position for the check - piece.x = originalX + kickX - piece.y = originalY + kickY - if (canMove(0, 0)) { // Check validity at the kicked position - Log.d(TAG, "Rotate CCW kick applied: ($kickX, $kickY), new pos: (${piece.x}, ${piece.y})") - kickApplied = true - onPieceMove?.invoke() - break // Found a valid kick, stop checking - } - } - - // Revert if no kick worked - if (!kickApplied) { - Log.d(TAG, "Rotate CCW failed - reverting rotation and position") - piece.setRotationIndex(originalRotationIndex) // Use setter - piece.x = originalX // Revert position - piece.y = originalY - // Do not call onPieceMove if rotation failed - } - // No need for onPieceMove here as it's called when kick succeeds or initial rotation is fine + if (!canMove(0, 0)) { + // Try to move left + if (canMove(-1, 0)) { + it.x-- + } + // Try to move right + else if (canMove(1, 0)) { + it.x++ + } + // Try to move 2 spaces (for I piece) + else if (canMove(-2, 0)) { + it.x -= 2 + } + else if (canMove(2, 0)) { + it.x += 2 + } + // Try to move up for floor kicks + else if (canMove(0, -1)) { + it.y-- + } + // Revert if can't find a valid position + else { + it.rotateClockwise() + it.x = originalX + it.y = originalY + } + } } } @@ -409,8 +368,7 @@ class GameBoard( val boardY = newY + y // Check if the position is outside the board horizontally - val isOutsideHorizontal = boardX < 0 || boardX >= width - if (isOutsideHorizontal) { + if (boardX < 0 || boardX >= width) { return false } @@ -424,8 +382,8 @@ class GameBoard( return false } - // Check if the position is above the board (top wall collision) - if (boardY < 0) { + // Check if the position is more than one unit above the top of the screen + if (boardY < -1) { return false } } diff --git a/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt b/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt index f0a02d6..6f2d708 100644 --- a/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt +++ b/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt @@ -17,21 +17,6 @@ class Tetromino(val type: TetrominoType) { var x = 0 var y = 0 - /** - * Get the current rotation index (0-3) - */ - fun getRotationIndex(): Int { - return currentRotation - } - - /** - * Set the current rotation index (0-3) - * Internal visibility allows access within the same module (e.g., from GameBoard) - */ - internal fun setRotationIndex(index: Int) { - currentRotation = index.coerceIn(0, 3) // Ensure value stays within 0-3 - } - /** * Get the current shape of the tetromino based on rotation */ diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 715a14d..eb8a729 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -70,7 +70,7 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - android:layout_marginTop="52dp"/> + android:layout_marginTop="24dp"/> + android:layout_marginTop="24dp"/> - - @@ -144,7 +141,7 @@ android:layout_width="60dp" android:layout_height="60dp" android:layout_marginStart="16dp" - android:layout_marginTop="48dp" + android:layout_marginTop="16dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/high_score_entry.xml b/app/src/main/res/layout/high_score_entry.xml index 9096d10..68a32a5 100644 --- a/app/src/main/res/layout/high_score_entry.xml +++ b/app/src/main/res/layout/high_score_entry.xml @@ -26,15 +26,6 @@ android:fontFamily="monospace" android:layout_marginBottom="16dp"/> - -