2025-03-26 12:44:00 -04:00
|
|
|
package com.mintris.game
|
|
|
|
|
|
|
|
import android.animation.ValueAnimator
|
|
|
|
import android.content.Context
|
|
|
|
import android.graphics.Canvas
|
|
|
|
import android.graphics.Color
|
|
|
|
import android.graphics.Paint
|
|
|
|
import android.graphics.Rect
|
|
|
|
import android.graphics.RectF
|
2025-03-26 16:09:03 -04:00
|
|
|
import android.graphics.BlurMaskFilter
|
2025-03-26 12:44:00 -04:00
|
|
|
import android.os.Build
|
|
|
|
import android.os.Handler
|
|
|
|
import android.os.Looper
|
|
|
|
import android.util.AttributeSet
|
2025-03-28 12:33:42 -04:00
|
|
|
import android.util.Log
|
2025-03-26 12:44:00 -04:00
|
|
|
import android.view.MotionEvent
|
|
|
|
import android.view.View
|
|
|
|
import android.view.animation.LinearInterpolator
|
|
|
|
import android.hardware.display.DisplayManager
|
2025-03-28 12:33:42 -04:00
|
|
|
import android.view.Display
|
2025-03-26 12:44:00 -04:00
|
|
|
import com.mintris.model.GameBoard
|
|
|
|
import com.mintris.model.Tetromino
|
|
|
|
import com.mintris.model.TetrominoType
|
|
|
|
import kotlin.math.abs
|
|
|
|
import kotlin.math.min
|
|
|
|
|
|
|
|
/**
|
|
|
|
* GameView that renders the Tetris game and handles touch input
|
|
|
|
*/
|
|
|
|
class GameView @JvmOverloads constructor(
|
|
|
|
context: Context,
|
|
|
|
attrs: AttributeSet? = null,
|
|
|
|
defStyleAttr: Int = 0
|
|
|
|
) : View(context, attrs, defStyleAttr) {
|
|
|
|
|
2025-03-28 12:33:42 -04:00
|
|
|
companion object {
|
|
|
|
private const val TAG = "GameView"
|
|
|
|
}
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
// Game board model
|
2025-03-26 16:09:03 -04:00
|
|
|
private var gameBoard = GameBoard()
|
|
|
|
private var gameHaptics: GameHaptics? = null
|
2025-03-26 12:44:00 -04:00
|
|
|
|
|
|
|
// Game state
|
|
|
|
private var isRunning = false
|
2025-03-28 11:57:21 -04:00
|
|
|
var isPaused = false // Changed from private to public to allow access from MainActivity
|
|
|
|
private var score = 0
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-26 16:09:03 -04:00
|
|
|
// Callbacks
|
|
|
|
var onNextPieceChanged: (() -> Unit)? = null
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
// Rendering
|
|
|
|
private val blockPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
isAntiAlias = true
|
|
|
|
}
|
|
|
|
|
|
|
|
private val ghostBlockPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
alpha = 80 // 30% opacity
|
|
|
|
isAntiAlias = true
|
|
|
|
}
|
|
|
|
|
|
|
|
private val gridPaint = Paint().apply {
|
|
|
|
color = Color.parseColor("#222222") // Very dark gray
|
2025-03-26 16:09:03 -04:00
|
|
|
alpha = 20 // Reduced from 40 to be more subtle
|
2025-03-26 12:44:00 -04:00
|
|
|
isAntiAlias = true
|
|
|
|
strokeWidth = 1f
|
|
|
|
style = Paint.Style.STROKE
|
2025-03-27 01:54:55 -04:00
|
|
|
maskFilter = null // Ensure no blur effect on grid lines
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private val glowPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
2025-03-26 16:09:03 -04:00
|
|
|
alpha = 40 // Reduced from 80 for more subtlety
|
2025-03-26 12:44:00 -04:00
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.STROKE
|
2025-03-26 16:09:03 -04:00
|
|
|
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)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private val borderGlowPaint = Paint().apply {
|
2025-03-26 16:09:03 -04:00
|
|
|
color = Color.WHITE
|
|
|
|
alpha = 60
|
2025-03-26 12:44:00 -04:00
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.STROKE
|
2025-03-26 16:09:03 -04:00
|
|
|
strokeWidth = 2f
|
|
|
|
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// Add a new paint for the pulse effect
|
|
|
|
private val pulsePaint = Paint().apply {
|
|
|
|
color = Color.CYAN
|
|
|
|
alpha = 255
|
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.FILL
|
|
|
|
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f
|
|
|
|
}
|
|
|
|
|
2025-03-26 19:39:14 -04:00
|
|
|
// Pre-allocate paint objects to avoid GC
|
|
|
|
private val tmpPaint = Paint()
|
2025-03-26 12:44:00 -04:00
|
|
|
|
|
|
|
// Calculate block size based on view dimensions and board size
|
|
|
|
private var blockSize = 0f
|
|
|
|
private var boardLeft = 0f
|
|
|
|
private var boardTop = 0f
|
|
|
|
|
|
|
|
// Game loop handler and runnable
|
|
|
|
private val handler = Handler(Looper.getMainLooper())
|
|
|
|
private val gameLoopRunnable = object : Runnable {
|
|
|
|
override fun run() {
|
|
|
|
if (isRunning && !isPaused) {
|
|
|
|
update()
|
|
|
|
invalidate()
|
|
|
|
handler.postDelayed(this, gameBoard.dropInterval)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Touch parameters
|
|
|
|
private var lastTouchX = 0f
|
|
|
|
private var lastTouchY = 0f
|
|
|
|
private var startX = 0f
|
|
|
|
private var startY = 0f
|
|
|
|
private var lastTapTime = 0L
|
|
|
|
private var lastRotationTime = 0L
|
2025-03-26 16:09:03 -04:00
|
|
|
private var lastMoveTime = 0L
|
2025-03-28 18:59:13 -04:00
|
|
|
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
|
2025-03-28 11:57:21 -04:00
|
|
|
private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes
|
2025-03-26 12:44:00 -04:00
|
|
|
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)
|
2025-03-26 16:09:03 -04:00
|
|
|
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
|
2025-03-27 20:47:20 -04:00
|
|
|
private var lockedDirection: Direction? = null // Track the locked movement direction
|
|
|
|
private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size
|
2025-03-28 11:57:21 -04:00
|
|
|
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
|
2025-03-27 20:47:20 -04:00
|
|
|
|
2025-03-28 15:45:20 -04:00
|
|
|
// Block skin
|
|
|
|
private var currentBlockSkin: String = "block_skin_1"
|
|
|
|
private val blockSkinPaints = mutableMapOf<String, Paint>()
|
|
|
|
|
2025-03-27 20:47:20 -04:00
|
|
|
private enum class Direction {
|
|
|
|
HORIZONTAL, VERTICAL
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
|
|
|
|
// Callback for game events
|
|
|
|
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
|
|
|
|
var onGameOver: ((score: Int) -> Unit)? = null
|
|
|
|
var onLineClear: ((Int) -> Unit)? = null // New callback for line clear events
|
|
|
|
var onPieceMove: (() -> Unit)? = null // New callback for piece movement
|
|
|
|
var onPieceLock: (() -> Unit)? = null // New callback for piece locking
|
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// Animation state
|
|
|
|
private var pulseAnimator: ValueAnimator? = null
|
|
|
|
private var pulseAlpha = 0f
|
|
|
|
private var isPulsing = false
|
|
|
|
private var linesToPulse = mutableListOf<Int>() // Track which lines are being cleared
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
init {
|
|
|
|
// Start with paused state
|
|
|
|
pause()
|
|
|
|
|
2025-03-28 19:36:14 -04:00
|
|
|
// Load saved block skin
|
|
|
|
val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE)
|
|
|
|
currentBlockSkin = prefs.getString("selected_block_skin", "block_skin_1") ?: "block_skin_1"
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
// Connect our callbacks to the GameBoard
|
|
|
|
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
2025-03-28 18:59:13 -04:00
|
|
|
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()
|
|
|
|
}
|
2025-03-27 01:54:55 -04:00
|
|
|
gameBoard.onLineClear = { lineCount, clearedLines ->
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Received line clear from GameBoard: $lineCount lines")
|
2025-03-27 00:54:04 -04:00
|
|
|
try {
|
|
|
|
onLineClear?.invoke(lineCount)
|
2025-03-27 01:54:55 -04:00
|
|
|
// Use the lines that were cleared directly
|
|
|
|
linesToPulse.clear()
|
|
|
|
linesToPulse.addAll(clearedLines)
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
|
2025-03-27 01:54:55 -04:00
|
|
|
startPulseAnimation(lineCount)
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Forwarded line clear callback")
|
2025-03-27 00:54:04 -04:00
|
|
|
} catch (e: Exception) {
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.e(TAG, "Error forwarding line clear callback", e)
|
2025-03-27 00:54:04 -04:00
|
|
|
}
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-26 19:39:14 -04:00
|
|
|
// Force hardware acceleration - This is critical for performance
|
2025-03-26 12:44:00 -04:00
|
|
|
setLayerType(LAYER_TYPE_HARDWARE, null)
|
|
|
|
|
|
|
|
// Set better frame rate using modern APIs
|
|
|
|
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
2025-03-28 12:33:42 -04:00
|
|
|
val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
|
|
displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
|
|
|
} else {
|
|
|
|
displayManager.displays.firstOrNull()
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
display?.let { disp ->
|
|
|
|
val refreshRate = disp.refreshRate
|
|
|
|
// Set game loop interval based on refresh rate, but don't go faster than the base interval
|
|
|
|
val targetFps = refreshRate.toInt()
|
|
|
|
if (targetFps > 0) {
|
|
|
|
gameBoard.dropInterval = gameBoard.dropInterval.coerceAtMost(1000L / targetFps)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enable edge-to-edge rendering
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
|
|
setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height)))
|
|
|
|
}
|
2025-03-28 15:45:20 -04:00
|
|
|
|
|
|
|
// 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
|
2025-03-28 19:36:14 -04:00
|
|
|
// Save the selection to SharedPreferences
|
|
|
|
val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE)
|
|
|
|
prefs.edit().putString("selected_block_skin", skinId).commit()
|
2025-03-28 15:45:20 -04:00
|
|
|
invalidate()
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
2025-03-28 15:45:20 -04:00
|
|
|
/**
|
|
|
|
* Get the current block skin
|
|
|
|
*/
|
|
|
|
fun getCurrentBlockSkin(): String = currentBlockSkin
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
/**
|
|
|
|
* Start the game
|
|
|
|
*/
|
|
|
|
fun start() {
|
2025-03-26 16:09:03 -04:00
|
|
|
isPaused = false
|
|
|
|
isRunning = true
|
|
|
|
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
|
|
|
handler.post(gameLoopRunnable)
|
|
|
|
invalidate()
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pause the game
|
|
|
|
*/
|
|
|
|
fun pause() {
|
|
|
|
isPaused = true
|
|
|
|
handler.removeCallbacks(gameLoopRunnable)
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset the game
|
|
|
|
*/
|
|
|
|
fun reset() {
|
|
|
|
isRunning = false
|
|
|
|
isPaused = true
|
|
|
|
gameBoard.reset()
|
2025-03-26 16:09:03 -04:00
|
|
|
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
2025-03-26 12:44:00 -04:00
|
|
|
handler.removeCallbacks(gameLoopRunnable)
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update game state (called on game loop)
|
|
|
|
*/
|
|
|
|
private fun update() {
|
|
|
|
if (gameBoard.isGameOver) {
|
|
|
|
isRunning = false
|
|
|
|
isPaused = true
|
|
|
|
onGameOver?.invoke(gameBoard.score)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-03-28 14:35:20 -04:00
|
|
|
// Update the game state
|
|
|
|
gameBoard.update()
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-27 00:27:44 -04:00
|
|
|
// Update UI with current game state
|
|
|
|
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
|
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
|
|
|
2025-03-26 19:39:14 -04:00
|
|
|
// Force hardware acceleration - Critical for performance
|
2025-03-26 12:44:00 -04:00
|
|
|
setLayerType(LAYER_TYPE_HARDWARE, null)
|
|
|
|
|
|
|
|
// Update gesture exclusion rect for edge-to-edge rendering
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
|
|
setSystemGestureExclusionRects(listOf(Rect(0, 0, w, h)))
|
|
|
|
}
|
|
|
|
|
|
|
|
calculateDimensions(w, h)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// Account for all glow effects and borders
|
|
|
|
val borderPadding = 16f // Padding for border glow effects
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// 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
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDraw(canvas: Canvas) {
|
2025-03-26 19:39:14 -04:00
|
|
|
// Skip drawing if paused or game over - faster return
|
|
|
|
if (isPaused || gameBoard.isGameOver) {
|
|
|
|
super.onDraw(canvas)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set hardware layer type during draw for better performance
|
|
|
|
val wasHardwareAccelerated = isHardwareAccelerated
|
|
|
|
if (!wasHardwareAccelerated) {
|
|
|
|
setLayerType(LAYER_TYPE_HARDWARE, null)
|
|
|
|
}
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
super.onDraw(canvas)
|
|
|
|
|
|
|
|
// Draw background (already black from theme)
|
|
|
|
|
|
|
|
// Draw board border glow
|
|
|
|
drawBoardBorder(canvas)
|
|
|
|
|
|
|
|
// Draw grid (very subtle)
|
|
|
|
drawGrid(canvas)
|
|
|
|
|
2025-03-26 19:39:14 -04:00
|
|
|
// Draw locked pieces
|
|
|
|
drawLockedBlocks(canvas)
|
2025-03-26 12:44:00 -04:00
|
|
|
|
|
|
|
if (!gameBoard.isGameOver && isRunning) {
|
|
|
|
// Draw ghost piece (landing preview)
|
|
|
|
drawGhostPiece(canvas)
|
|
|
|
|
|
|
|
// Draw active piece
|
|
|
|
drawActivePiece(canvas)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw glowing border around the playable area
|
|
|
|
*/
|
|
|
|
private fun drawBoardBorder(canvas: Canvas) {
|
|
|
|
val left = boardLeft
|
|
|
|
val top = boardTop
|
|
|
|
val right = boardLeft + gameBoard.width * blockSize
|
|
|
|
val bottom = boardTop + gameBoard.height * blockSize
|
|
|
|
|
|
|
|
val rect = RectF(left, top, right, bottom)
|
2025-03-27 01:54:55 -04:00
|
|
|
|
|
|
|
// Draw base border with increased glow
|
|
|
|
borderGlowPaint.apply {
|
|
|
|
alpha = 80 // Increased from 60
|
|
|
|
maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
canvas.drawRect(rect, borderGlowPaint)
|
2025-03-27 01:54:55 -04:00
|
|
|
|
|
|
|
// Draw pulsing border if animation is active
|
|
|
|
if (isPulsing) {
|
|
|
|
val pulseBorderPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
style = Paint.Style.STROKE
|
|
|
|
strokeWidth = 6f + (16f * pulseAlpha) // Increased from 4f+12f to 6f+16f
|
|
|
|
alpha = (255 * pulseAlpha).toInt()
|
|
|
|
isAntiAlias = true
|
|
|
|
maskFilter = BlurMaskFilter(32f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Increased from 24f to 32f
|
|
|
|
}
|
|
|
|
// Draw the border with a slight inset to prevent edge artifacts
|
|
|
|
val inset = 1f
|
|
|
|
canvas.drawRect(
|
|
|
|
left + inset,
|
|
|
|
top + inset,
|
|
|
|
right - inset,
|
|
|
|
bottom - inset,
|
|
|
|
pulseBorderPaint
|
|
|
|
)
|
|
|
|
|
|
|
|
// Add an additional outer glow for more dramatic effect
|
|
|
|
val outerGlowPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
style = Paint.Style.STROKE
|
|
|
|
strokeWidth = 2f
|
|
|
|
alpha = (128 * pulseAlpha).toInt()
|
|
|
|
isAntiAlias = true
|
|
|
|
maskFilter = BlurMaskFilter(48f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
|
|
|
|
}
|
|
|
|
canvas.drawRect(
|
|
|
|
left - 4f,
|
|
|
|
top - 4f,
|
|
|
|
right + 4f,
|
|
|
|
bottom + 4f,
|
|
|
|
outerGlowPaint
|
|
|
|
)
|
2025-03-27 01:59:47 -04:00
|
|
|
|
|
|
|
// Add extra bright glow for side borders during line clear
|
|
|
|
val sideGlowPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
style = Paint.Style.STROKE
|
|
|
|
strokeWidth = 8f + (24f * pulseAlpha) // Thicker stroke for side borders
|
|
|
|
alpha = (255 * pulseAlpha).toInt()
|
|
|
|
isAntiAlias = true
|
|
|
|
maskFilter = BlurMaskFilter(64f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Larger blur for side borders
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw left border with extra glow
|
|
|
|
canvas.drawLine(
|
|
|
|
left + inset,
|
|
|
|
top + inset,
|
|
|
|
left + inset,
|
|
|
|
bottom - inset,
|
|
|
|
sideGlowPaint
|
|
|
|
)
|
|
|
|
|
|
|
|
// Draw right border with extra glow
|
|
|
|
canvas.drawLine(
|
|
|
|
right - inset,
|
|
|
|
top + inset,
|
|
|
|
right - inset,
|
|
|
|
bottom - inset,
|
|
|
|
sideGlowPaint
|
|
|
|
)
|
2025-03-27 01:54:55 -04:00
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw the grid lines (very subtle)
|
|
|
|
*/
|
|
|
|
private fun drawGrid(canvas: Canvas) {
|
2025-03-27 01:54:55 -04:00
|
|
|
// Save the canvas state to prevent any effects from affecting the grid
|
|
|
|
canvas.save()
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
// Draw vertical grid lines
|
|
|
|
for (x in 0..gameBoard.width) {
|
|
|
|
val xPos = boardLeft + x * blockSize
|
|
|
|
canvas.drawLine(
|
|
|
|
xPos, boardTop,
|
|
|
|
xPos, boardTop + gameBoard.height * blockSize,
|
|
|
|
gridPaint
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw horizontal grid lines
|
|
|
|
for (y in 0..gameBoard.height) {
|
|
|
|
val yPos = boardTop + y * blockSize
|
|
|
|
canvas.drawLine(
|
|
|
|
boardLeft, yPos,
|
|
|
|
boardLeft + gameBoard.width * blockSize, yPos,
|
|
|
|
gridPaint
|
|
|
|
)
|
|
|
|
}
|
2025-03-27 01:54:55 -04:00
|
|
|
|
|
|
|
// Restore the canvas state
|
|
|
|
canvas.restore()
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw the locked blocks on the board
|
|
|
|
*/
|
|
|
|
private fun drawLockedBlocks(canvas: Canvas) {
|
|
|
|
for (y in 0 until gameBoard.height) {
|
|
|
|
for (x in 0 until gameBoard.width) {
|
|
|
|
if (gameBoard.isOccupied(x, y)) {
|
2025-03-27 01:54:55 -04:00
|
|
|
drawBlock(canvas, x, y, false, y in linesToPulse)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw the currently active tetromino
|
|
|
|
*/
|
|
|
|
private fun drawActivePiece(canvas: Canvas) {
|
|
|
|
val piece = gameBoard.getCurrentPiece() ?: return
|
|
|
|
|
|
|
|
for (y in 0 until piece.getHeight()) {
|
|
|
|
for (x in 0 until piece.getWidth()) {
|
|
|
|
if (piece.isBlockAt(x, y)) {
|
|
|
|
val boardX = piece.x + x
|
|
|
|
val boardY = piece.y + y
|
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// Draw piece regardless of vertical position
|
|
|
|
if (boardX >= 0 && boardX < gameBoard.width) {
|
|
|
|
drawBlock(canvas, boardX, boardY, false, false)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw the ghost piece (landing preview)
|
|
|
|
*/
|
|
|
|
private fun drawGhostPiece(canvas: Canvas) {
|
|
|
|
val piece = gameBoard.getCurrentPiece() ?: return
|
|
|
|
val ghostY = gameBoard.getGhostY()
|
|
|
|
|
|
|
|
for (y in 0 until piece.getHeight()) {
|
|
|
|
for (x in 0 until piece.getWidth()) {
|
|
|
|
if (piece.isBlockAt(x, y)) {
|
|
|
|
val boardX = piece.x + x
|
|
|
|
val boardY = ghostY + y
|
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// Draw ghost piece regardless of vertical position
|
|
|
|
if (boardX >= 0 && boardX < gameBoard.width) {
|
|
|
|
drawBlock(canvas, boardX, boardY, true, false)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw a single tetris block at the given grid position
|
|
|
|
*/
|
2025-03-27 01:54:55 -04:00
|
|
|
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
|
2025-03-26 12:44:00 -04:00
|
|
|
val left = boardLeft + x * blockSize
|
|
|
|
val top = boardTop + y * blockSize
|
|
|
|
val right = left + blockSize
|
|
|
|
val bottom = top + blockSize
|
|
|
|
|
2025-03-27 01:54:55 -04:00
|
|
|
// Save canvas state before drawing block effects
|
|
|
|
canvas.save()
|
|
|
|
|
2025-03-28 15:45:20 -04:00
|
|
|
// Get the current block skin paint
|
|
|
|
val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!!
|
|
|
|
|
2025-03-28 16:11:30 -04:00
|
|
|
// 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)
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
2025-03-27 01:54:55 -04:00
|
|
|
|
|
|
|
// Draw pulse effect if animation is active and this is a pulsing line
|
|
|
|
if (isPulsing && isPulsingLine) {
|
|
|
|
val pulseBlockPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
alpha = (255 * pulseAlpha).toInt()
|
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.FILL
|
|
|
|
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
|
|
|
|
}
|
|
|
|
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Restore canvas state after drawing block effects
|
|
|
|
canvas.restore()
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the given board position is part of the current piece
|
|
|
|
*/
|
|
|
|
private fun isPositionInPiece(boardX: Int, boardY: Int, piece: Tetromino): Boolean {
|
|
|
|
for (y in 0 until piece.getHeight()) {
|
|
|
|
for (x in 0 until piece.getWidth()) {
|
|
|
|
if (piece.isBlockAt(x, y)) {
|
|
|
|
val pieceX = piece.x + x
|
|
|
|
val pieceY = piece.y + y
|
|
|
|
if (pieceX == boardX && pieceY == boardY) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get color for tetromino type
|
|
|
|
*/
|
|
|
|
private fun getTetrominoColor(type: TetrominoType): Int {
|
|
|
|
return when (type) {
|
|
|
|
TetrominoType.I -> Color.CYAN
|
|
|
|
TetrominoType.J -> Color.BLUE
|
|
|
|
TetrominoType.L -> Color.rgb(255, 165, 0) // Orange
|
|
|
|
TetrominoType.O -> Color.YELLOW
|
|
|
|
TetrominoType.S -> Color.GREEN
|
|
|
|
TetrominoType.T -> Color.MAGENTA
|
|
|
|
TetrominoType.Z -> Color.RED
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Custom touch event handling
|
|
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
|
|
if (!isRunning || isPaused || gameBoard.isGameOver) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2025-03-28 18:59:13 -04:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
when (event.action) {
|
|
|
|
MotionEvent.ACTION_DOWN -> {
|
|
|
|
// Record start of touch
|
|
|
|
startX = event.x
|
|
|
|
startY = event.y
|
|
|
|
lastTouchX = event.x
|
|
|
|
lastTouchY = event.y
|
2025-03-27 20:47:20 -04:00
|
|
|
lockedDirection = null // Reset direction lock
|
2025-03-26 12:44:00 -04:00
|
|
|
|
|
|
|
// Check for double tap (rotate)
|
|
|
|
val currentTime = System.currentTimeMillis()
|
2025-03-26 16:09:03 -04:00
|
|
|
if (currentTime - lastTapTime < 200) { // Reduced from 250ms for faster response
|
2025-03-26 12:44:00 -04:00
|
|
|
// Double tap detected, rotate the piece
|
|
|
|
if (currentTime - lastRotationTime >= rotationCooldown) {
|
|
|
|
gameBoard.rotate()
|
|
|
|
lastRotationTime = currentTime
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
lastTapTime = currentTime
|
|
|
|
}
|
|
|
|
|
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
|
|
val deltaX = event.x - lastTouchX
|
|
|
|
val deltaY = event.y - lastTouchY
|
2025-03-26 16:09:03 -04:00
|
|
|
val currentTime = System.currentTimeMillis()
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-27 20:47:20 -04:00
|
|
|
// Determine movement direction if not locked
|
|
|
|
if (lockedDirection == null) {
|
|
|
|
val absDeltaX = abs(deltaX)
|
|
|
|
val absDeltaY = abs(deltaY)
|
|
|
|
|
|
|
|
// Check if movement exceeds threshold
|
|
|
|
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) {
|
2025-03-28 11:57:21 -04:00
|
|
|
// Determine dominant direction with stricter criteria
|
2025-03-27 20:47:20 -04:00
|
|
|
if (absDeltaX > absDeltaY * directionLockThreshold) {
|
|
|
|
lockedDirection = Direction.HORIZONTAL
|
|
|
|
} else if (absDeltaY > absDeltaX * directionLockThreshold) {
|
|
|
|
lockedDirection = Direction.VERTICAL
|
|
|
|
}
|
2025-03-28 11:57:21 -04:00
|
|
|
// 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
|
2025-03-26 16:09:03 -04:00
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
2025-03-27 20:47:20 -04:00
|
|
|
// Handle movement based on locked direction
|
|
|
|
when (lockedDirection) {
|
|
|
|
Direction.HORIZONTAL -> {
|
|
|
|
if (abs(deltaX) > blockSize * minMovementThreshold) {
|
|
|
|
if (deltaX > 0) {
|
|
|
|
gameBoard.moveRight()
|
|
|
|
} else {
|
|
|
|
gameBoard.moveLeft()
|
|
|
|
}
|
|
|
|
lastTouchX = event.x
|
|
|
|
if (currentTime - lastMoveTime >= moveCooldown) {
|
|
|
|
gameHaptics?.vibrateForPieceMove()
|
|
|
|
lastMoveTime = currentTime
|
|
|
|
}
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Direction.VERTICAL -> {
|
|
|
|
if (deltaY > blockSize * minMovementThreshold) {
|
2025-03-28 14:35:20 -04:00
|
|
|
gameBoard.softDrop()
|
2025-03-27 20:47:20 -04:00
|
|
|
lastTouchY = event.y
|
|
|
|
if (currentTime - lastMoveTime >= moveCooldown) {
|
|
|
|
gameHaptics?.vibrateForPieceMove()
|
|
|
|
lastMoveTime = currentTime
|
|
|
|
}
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
null -> {
|
|
|
|
// No direction lock yet, don't process movement
|
2025-03-26 16:09:03 -04:00
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2025-03-28 18:59:13 -04:00
|
|
|
val currentTime = System.currentTimeMillis()
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-28 18:59:13 -04:00
|
|
|
// Check if this might have been a hard drop gesture
|
|
|
|
val isVerticalSwipe = moveTime > 0 &&
|
|
|
|
deltaY > blockSize * minHardDropDistance &&
|
2025-03-28 11:57:21 -04:00
|
|
|
(deltaY / moveTime) * 1000 > minSwipeVelocity &&
|
2025-03-28 18:59:13 -04:00
|
|
|
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()
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
} else if (moveTime < minTapTime &&
|
|
|
|
abs(deltaY) < maxTapMovement &&
|
|
|
|
abs(deltaX) < maxTapMovement) {
|
|
|
|
// Quick tap with minimal movement (rotation)
|
|
|
|
if (currentTime - lastRotationTime >= rotationCooldown) {
|
2025-03-28 18:59:13 -04:00
|
|
|
Log.d("GameView", "Rotation detected")
|
2025-03-26 12:44:00 -04:00
|
|
|
gameBoard.rotate()
|
|
|
|
lastRotationTime = currentTime
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
2025-03-27 20:47:20 -04:00
|
|
|
|
|
|
|
// Reset direction lock
|
|
|
|
lockedDirection = null
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current score
|
|
|
|
*/
|
|
|
|
fun getScore(): Int = gameBoard.score
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current level
|
|
|
|
*/
|
|
|
|
fun getLevel(): Int = gameBoard.level
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the number of lines cleared
|
|
|
|
*/
|
|
|
|
fun getLines(): Int = gameBoard.lines
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the game is over
|
|
|
|
*/
|
|
|
|
fun isGameOver(): Boolean = gameBoard.isGameOver
|
|
|
|
|
|
|
|
/**
|
2025-03-26 16:09:03 -04:00
|
|
|
* Get the next piece that will be spawned
|
2025-03-26 12:44:00 -04:00
|
|
|
*/
|
2025-03-26 16:09:03 -04:00
|
|
|
fun getNextPiece(): Tetromino? {
|
|
|
|
return gameBoard.getNextPiece()
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up resources when view is detached
|
|
|
|
*/
|
|
|
|
override fun onDetachedFromWindow() {
|
|
|
|
super.onDetachedFromWindow()
|
|
|
|
handler.removeCallbacks(gameLoopRunnable)
|
|
|
|
}
|
2025-03-26 16:09:03 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the game board for this view
|
|
|
|
*/
|
|
|
|
fun setGameBoard(board: GameBoard) {
|
|
|
|
gameBoard = board
|
2025-03-27 00:54:04 -04:00
|
|
|
|
|
|
|
// Reconnect callbacks to the new board
|
|
|
|
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
|
|
|
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
2025-03-27 01:54:55 -04:00
|
|
|
gameBoard.onLineClear = { lineCount, clearedLines ->
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Received line clear from GameBoard: $lineCount lines")
|
2025-03-27 00:54:04 -04:00
|
|
|
try {
|
|
|
|
onLineClear?.invoke(lineCount)
|
2025-03-27 01:54:55 -04:00
|
|
|
// Use the lines that were cleared directly
|
|
|
|
linesToPulse.clear()
|
|
|
|
linesToPulse.addAll(clearedLines)
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
|
2025-03-27 01:54:55 -04:00
|
|
|
startPulseAnimation(lineCount)
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Forwarded line clear callback")
|
2025-03-27 00:54:04 -04:00
|
|
|
} catch (e: Exception) {
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.e(TAG, "Error forwarding line clear callback", e)
|
2025-03-27 00:54:04 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-26 16:09:03 -04:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the haptics handler for this view
|
|
|
|
*/
|
|
|
|
fun setHaptics(haptics: GameHaptics) {
|
|
|
|
gameHaptics = haptics
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resume the game
|
|
|
|
*/
|
|
|
|
fun resume() {
|
|
|
|
if (!isRunning) {
|
|
|
|
isRunning = true
|
|
|
|
}
|
|
|
|
isPaused = false
|
2025-03-26 19:13:43 -04:00
|
|
|
|
|
|
|
// Restart the game loop immediately
|
|
|
|
handler.removeCallbacks(gameLoopRunnable)
|
|
|
|
handler.post(gameLoopRunnable)
|
|
|
|
|
|
|
|
// Force an update to ensure pieces move immediately
|
|
|
|
update()
|
2025-03-26 16:09:03 -04:00
|
|
|
invalidate()
|
|
|
|
}
|
2025-03-27 01:54:55 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the pulse animation for line clear
|
|
|
|
*/
|
|
|
|
private fun startPulseAnimation(lineCount: Int) {
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Starting pulse animation for $lineCount lines")
|
2025-03-27 01:54:55 -04:00
|
|
|
|
|
|
|
// Cancel any existing animation
|
|
|
|
pulseAnimator?.cancel()
|
|
|
|
|
|
|
|
// Create new animation
|
|
|
|
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
|
|
|
|
duration = when (lineCount) {
|
|
|
|
4 -> 2000L // Tetris - longer duration
|
|
|
|
3 -> 1600L // Triples
|
|
|
|
2 -> 1200L // Doubles
|
|
|
|
1 -> 1000L // Singles
|
|
|
|
else -> 1000L
|
|
|
|
}
|
|
|
|
interpolator = LinearInterpolator()
|
|
|
|
addUpdateListener { animation ->
|
|
|
|
pulseAlpha = animation.animatedValue as Float
|
|
|
|
isPulsing = true
|
|
|
|
invalidate()
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha")
|
2025-03-27 01:54:55 -04:00
|
|
|
}
|
|
|
|
addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
|
|
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
|
|
isPulsing = false
|
|
|
|
pulseAlpha = 0f
|
|
|
|
linesToPulse.clear()
|
|
|
|
invalidate()
|
2025-03-28 12:33:42 -04:00
|
|
|
Log.d(TAG, "Pulse animation ended")
|
2025-03-27 01:54:55 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
pulseAnimator?.start()
|
|
|
|
}
|
2025-03-26 19:39:14 -04:00
|
|
|
}
|