Compare commits

..

2 commits

5 changed files with 237 additions and 57 deletions

View file

@ -172,6 +172,15 @@ class MainActivity : AppCompatActivity() {
binding.nextPieceView.invalidate() 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 // Set up music toggle
binding.musicToggle.setOnClickListener { binding.musicToggle.setOnClickListener {
isMusicEnabled = !isMusicEnabled isMusicEnabled = !isMusicEnabled

View file

@ -140,15 +140,21 @@ class GameView @JvmOverloads constructor(
private var touchFreezeUntil = 0L // Time until which touch events should be ignored 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 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 = 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 minTapTime = 100L // Minimum time for a tap (in milliseconds)
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
private 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 var lockedDirection: Direction? = null // Track the locked movement direction
private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size private val minMovementThreshold = 0.5f // Reduced from 0.75f for more sensitive movement
private val directionLockThreshold = 2.5f // Increased from 1.5f to make direction locking more aggressive private val directionLockThreshold = 1.5f // Reduced from 2.5f to make direction locking less aggressive
private val isStrictDirectionLock = true // Enable strict direction locking to prevent diagonal inputs 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 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 // Block skin
private var currentBlockSkin: String = "block_skin_1" private var currentBlockSkin: String = "block_skin_1"
@ -803,46 +809,33 @@ class GameView @JvmOverloads constructor(
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
// Record start of touch
startX = event.x startX = event.x
startY = event.y startY = event.y
lastTouchX = event.x lastTouchX = event.x
lastTouchY = event.y lastTouchY = event.y
lockedDirection = null // Reset direction lock lastTapTime = currentTime // Set the tap time when touch starts
// Check for double tap (rotate) // Reset direction lock
val currentTime = System.currentTimeMillis() lockedDirection = null
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
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - lastTouchX val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY val deltaY = event.y - lastTouchY
val currentTime = System.currentTimeMillis()
// Determine movement direction if not locked // Check if we should lock direction
if (lockedDirection == null) { if (lockedDirection == null) {
val absDeltaX = abs(deltaX) val absDeltaX = abs(deltaX)
val absDeltaY = abs(deltaY) val absDeltaY = abs(deltaY)
// Check if movement exceeds threshold if (absDeltaX > blockSize * directionLockThreshold ||
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) { absDeltaY > blockSize * directionLockThreshold) {
// Determine dominant direction with stricter criteria // Lock to the dominant direction
if (absDeltaX > absDeltaY * directionLockThreshold) { lockedDirection = if (absDeltaX > absDeltaY) {
lockedDirection = Direction.HORIZONTAL Direction.HORIZONTAL
} else if (absDeltaY > absDeltaX * directionLockThreshold) { } else {
lockedDirection = Direction.VERTICAL 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 -> { 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 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 // Check for hold gesture (swipe up)
val isVerticalSwipe = moveTime > 0 && if (deltaY < -blockSize * minHoldDistance &&
deltaY > blockSize * minHardDropDistance && abs(deltaX) / abs(deltaY) < 0.5f) {
(deltaY / moveTime) * 1000 > minSwipeVelocity && if (currentTime - lastHoldTime < holdCooldown) {
abs(deltaX) < abs(deltaY) * 0.3f Log.d(TAG, "Hold blocked by cooldown - time since last: ${currentTime - lastHoldTime}ms, cooldown: ${holdCooldown}ms")
// 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 { } else {
// Process the hard drop // Process the hold
Log.d("GameView", "Hard drop detected - deltaY: $deltaY, velocity: ${(deltaY / moveTime) * 1000}, ratio: ${abs(deltaX) / abs(deltaY)}") Log.d(TAG, "Hold detected - deltaY: $deltaY, ratio: ${abs(deltaX) / abs(deltaY)}")
gameBoard.hardDrop() gameBoard.holdPiece()
lastHardDropTime = currentTime // Update the last hard drop time lastHoldTime = currentTime
gameHaptics?.vibrateForPieceMove()
invalidate() 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(deltaY) < maxTapMovement &&
abs(deltaX) < maxTapMovement) { abs(deltaX) < maxTapMovement) {
// Quick tap with minimal movement (rotation)
if (currentTime - lastRotationTime >= rotationCooldown) { if (currentTime - lastRotationTime >= rotationCooldown) {
Log.d("GameView", "Rotation detected") Log.d(TAG, "Rotation detected - moveTime: $moveTime, deltaX: $deltaX, deltaY: $deltaY")
gameBoard.rotate() gameBoard.rotate()
lastRotationTime = currentTime lastRotationTime = currentTime
invalidate() invalidate()
@ -954,6 +952,11 @@ class GameView @JvmOverloads constructor(
return gameBoard.getNextPiece() return gameBoard.getNextPiece()
} }
/**
* Get the game board instance
*/
fun getGameBoard(): GameBoard = gameBoard
/** /**
* Clean up resources when view is detached * Clean up resources when view is detached
*/ */

View file

@ -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
)
}
}
}
}
}
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#FFFFFF" />
<solid android:color="#00000000" />
</shape>

View file

@ -99,12 +99,28 @@
</LinearLayout> </LinearLayout>
<!-- Next Piece Preview --> <!-- Next Piece Preview -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="end"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NEXT"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginBottom="4dp" />
<com.mintris.game.NextPieceView <com.mintris.game.NextPieceView
android:id="@+id/nextPieceView" android:id="@+id/nextPieceView"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="80dp" android:layout_height="80dp" />
android:layout_margin="16dp" </LinearLayout>
android:layout_gravity="end" />
<!-- Settings button --> <!-- Settings button -->
<ImageButton <ImageButton
@ -119,6 +135,16 @@
android:src="@drawable/ic_pause" /> android:src="@drawable/ic_pause" />
</LinearLayout> </LinearLayout>
<!-- Hold Piece Preview -->
<com.mintris.game.HoldPieceView
android:id="@+id/holdPieceView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Game Over overlay --> <!-- Game Over overlay -->
<LinearLayout <LinearLayout
android:id="@+id/gameOverContainer" android:id="@+id/gameOverContainer"