From 47c9bbddeccc787efde8a5a880289b34bc66f04d Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Sat, 29 Mar 2025 23:20:14 -0400 Subject: [PATCH 1/2] feat: change hold piece to swipe up gesture and improve movement sensitivity --- .../main/java/com/mintris/game/GameView.kt | 107 +++++++++--------- app/src/main/res/layout/activity_main.xml | 36 +++++- 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 1095af8..f1608f4 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -140,15 +140,21 @@ class GameView @JvmOverloads constructor( private var touchFreezeUntil = 0L // Time until which touch events should be ignored private val pieceLockFreezeTime = 300L // Time to freeze touch events after piece locks private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes - private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels) + private val maxTapMovement = 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 = 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 + private var lastTapY = 0f // Y coordinate of last tap + private var lastHoldTime = 0L // Track when the last hold occurred + private val holdCooldown = 250L // Minimum time between holds 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 minMovementThreshold = 0.5f // Reduced from 0.75f for more sensitive movement + private val directionLockThreshold = 1.5f // Reduced from 2.5f to make direction locking less aggressive + private val isStrictDirectionLock = true // Re-enabled strict direction locking to prevent diagonal inputs private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture + private val minHoldDistance = 2.0f // Minimum distance (in blocks) for hold gesture // Block skin private var currentBlockSkin: String = "block_skin_1" @@ -803,46 +809,33 @@ class GameView @JvmOverloads constructor( when (event.action) { MotionEvent.ACTION_DOWN -> { - // Record start of touch startX = event.x startY = event.y lastTouchX = event.x lastTouchY = event.y - lockedDirection = null // Reset direction lock + lastTapTime = currentTime // Set the tap time when touch starts - // Check for double tap (rotate) - val currentTime = System.currentTimeMillis() - if (currentTime - lastTapTime < 200) { // Reduced from 250ms for faster response - // Double tap detected, rotate the piece - if (currentTime - lastRotationTime >= rotationCooldown) { - gameBoard.rotate() - lastRotationTime = currentTime - invalidate() - } - } - lastTapTime = currentTime + // Reset direction lock + lockedDirection = null } MotionEvent.ACTION_MOVE -> { val deltaX = event.x - lastTouchX val deltaY = event.y - lastTouchY - val currentTime = System.currentTimeMillis() - // Determine movement direction if not locked + // Check if we should lock direction if (lockedDirection == null) { val absDeltaX = abs(deltaX) val absDeltaY = abs(deltaY) - // Check if movement exceeds threshold - if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { - // Determine dominant direction with stricter criteria - if (absDeltaX > absDeltaY * directionLockThreshold) { - lockedDirection = Direction.HORIZONTAL - } else if (absDeltaY > absDeltaX * directionLockThreshold) { - lockedDirection = Direction.VERTICAL + if (absDeltaX > blockSize * directionLockThreshold || + absDeltaY > blockSize * directionLockThreshold) { + // Lock to the dominant direction + lockedDirection = if (absDeltaX > absDeltaY) { + Direction.HORIZONTAL + } else { + 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 } } @@ -881,38 +874,43 @@ class GameView @JvmOverloads constructor( } MotionEvent.ACTION_UP -> { - // Calculate movement speed for potential fling detection - val moveTime = System.currentTimeMillis() - lastTapTime - val deltaY = event.y - startY val deltaX = event.x - startX - val currentTime = System.currentTimeMillis() + val deltaY = event.y - startY + val moveTime = currentTime - lastTapTime - // 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") + // Check for hold gesture (swipe up) + if (deltaY < -blockSize * minHoldDistance && + abs(deltaX) / abs(deltaY) < 0.5f) { + if (currentTime - lastHoldTime < holdCooldown) { + Log.d(TAG, "Hold blocked by cooldown - time since last: ${currentTime - lastHoldTime}ms, cooldown: ${holdCooldown}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 + // Process the hold + Log.d(TAG, "Hold detected - deltaY: $deltaY, ratio: ${abs(deltaX) / abs(deltaY)}") + gameBoard.holdPiece() + lastHoldTime = currentTime + gameHaptics?.vibrateForPieceMove() invalidate() } - } else if (moveTime < minTapTime && + } + // Check for hard drop + else if (deltaY > blockSize * minHardDropDistance && + abs(deltaX) / abs(deltaY) < 0.5f) { + if (currentTime - lastHardDropTime < hardDropCooldown) { + Log.d(TAG, "Hard drop blocked by cooldown - time since last: ${currentTime - lastHardDropTime}ms, cooldown: ${hardDropCooldown}ms") + } else { + // Process the hard drop + Log.d(TAG, "Hard drop detected - deltaY: $deltaY, velocity: ${(deltaY / moveTime) * 1000}, ratio: ${abs(deltaX) / abs(deltaY)}") + gameBoard.hardDrop() + lastHardDropTime = currentTime + invalidate() + } + } + // Check for rotation (quick tap with minimal movement) + else if (moveTime < minTapTime && abs(deltaY) < maxTapMovement && abs(deltaX) < maxTapMovement) { - // Quick tap with minimal movement (rotation) if (currentTime - lastRotationTime >= rotationCooldown) { - Log.d("GameView", "Rotation detected") + Log.d(TAG, "Rotation detected - moveTime: $moveTime, deltaX: $deltaX, deltaY: $deltaY") gameBoard.rotate() lastRotationTime = currentTime invalidate() @@ -954,6 +952,11 @@ class GameView @JvmOverloads constructor( return gameBoard.getNextPiece() } + /** + * Get the game board instance + */ + fun getGameBoard(): GameBoard = gameBoard + /** * Clean up resources when view is detached */ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2224e92..9b2b84a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -99,12 +99,28 @@ - + android:layout_gravity="end" + android:orientation="vertical"> + + + + + + + + Date: Sat, 29 Mar 2025 23:20:49 -0400 Subject: [PATCH 2/2] feat: implement hold piece functionality with swipe up gesture --- app/src/main/java/com/mintris/MainActivity.kt | 9 ++ .../java/com/mintris/game/HoldPieceView.kt | 134 ++++++++++++++++++ app/src/main/res/drawable/preview_border.xml | 8 ++ 3 files changed, 151 insertions(+) create mode 100644 app/src/main/java/com/mintris/game/HoldPieceView.kt create mode 100644 app/src/main/res/drawable/preview_border.xml diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 3e94bac..8b4602e 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -172,6 +172,15 @@ class MainActivity : AppCompatActivity() { binding.nextPieceView.invalidate() } + // Set up hold piece preview + binding.holdPieceView.setGameView(gameView) + gameBoard.onPieceLock = { + binding.holdPieceView.invalidate() + } + gameBoard.onPieceMove = { + binding.holdPieceView.invalidate() + } + // Set up music toggle binding.musicToggle.setOnClickListener { isMusicEnabled = !isMusicEnabled diff --git a/app/src/main/java/com/mintris/game/HoldPieceView.kt b/app/src/main/java/com/mintris/game/HoldPieceView.kt new file mode 100644 index 0000000..6f51022 --- /dev/null +++ b/app/src/main/java/com/mintris/game/HoldPieceView.kt @@ -0,0 +1,134 @@ +package com.mintris.game + +import android.content.Context +import android.graphics.BlurMaskFilter +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import com.mintris.model.GameBoard +import com.mintris.model.Tetromino +import kotlin.math.min + +/** + * View that displays the currently held piece + */ +class HoldPieceView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var gameView: GameView? = null + private var gameBoard: GameBoard? = null + + // Rendering + private val blockPaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + style = Paint.Style.FILL + } + + private val glowPaint = Paint().apply { + color = Color.WHITE + alpha = 40 + isAntiAlias = true + style = Paint.Style.STROKE + strokeWidth = 1.5f + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) + } + + private val blockGlowPaint = Paint().apply { + color = Color.WHITE + alpha = 60 + isAntiAlias = true + style = Paint.Style.FILL + maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) + } + + /** + * Set the game view reference + */ + fun setGameView(view: GameView) { + gameView = view + gameBoard = view.getGameBoard() + } + + /** + * Get the game board reference + */ + private fun getGameBoard(): GameBoard? = gameBoard + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Get the held piece from game board + gameBoard?.let { + it.getHoldPiece()?.let { piece -> + val width = piece.getWidth() + val height = piece.getHeight() + + // Calculate block size for the preview (smaller than main board) + val previewBlockSize = min( + canvas.width.toFloat() / (width + 2), + canvas.height.toFloat() / (height + 2) + ) + + // Center the piece in the preview area + val previewLeft = (canvas.width - width * previewBlockSize) / 2 + val previewTop = (canvas.height - height * previewBlockSize) / 2 + + // Draw subtle background glow + val glowPaint = Paint().apply { + color = Color.WHITE + alpha = 10 + maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER) + } + canvas.drawRect( + previewLeft - previewBlockSize, + previewTop - previewBlockSize, + previewLeft + width * previewBlockSize + previewBlockSize, + previewTop + height * previewBlockSize + previewBlockSize, + glowPaint + ) + + // Draw the held piece + for (y in 0 until height) { + for (x in 0 until width) { + if (piece.isBlockAt(x, y)) { + val left = previewLeft + x * previewBlockSize + val top = previewTop + y * previewBlockSize + val right = left + previewBlockSize + val bottom = top + previewBlockSize + + // Draw outer glow + blockGlowPaint.color = Color.WHITE + canvas.drawRect( + left - 2f, + top - 2f, + right + 2f, + bottom + 2f, + blockGlowPaint + ) + + // Draw block + blockPaint.color = Color.WHITE + canvas.drawRect(left, top, right, bottom, blockPaint) + + // Draw inner glow + glowPaint.color = Color.WHITE + canvas.drawRect( + left + 1f, + top + 1f, + right - 1f, + bottom - 1f, + glowPaint + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/preview_border.xml b/app/src/main/res/drawable/preview_border.xml new file mode 100644 index 0000000..a28f95b --- /dev/null +++ b/app/src/main/res/drawable/preview_border.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file