package com.mintris.game import android.animation.ValueAnimator import android.content.Context import android.graphics.BlurMaskFilter import android.graphics.Canvas import android.graphics.Color import android.graphics.LinearGradient import android.graphics.Paint import android.graphics.Rect import android.graphics.RectF import android.graphics.Shader 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.Display 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) { 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 // Callbacks var onNextPieceChanged: (() -> Unit)? = null // Rendering private val blockPaint = Paint().apply { color = Color.WHITE isAntiAlias = true } private val borderGlowPaint = Paint().apply { color = Color.WHITE alpha = 60 isAntiAlias = true style = Paint.Style.STROKE strokeWidth = 2f maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) } 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 alpha = 20 // Reduced from 40 to be more subtle isAntiAlias = true strokeWidth = 1f style = Paint.Style.STROKE maskFilter = null // Ensure no blur effect on grid lines } private val glowPaint = Paint().apply { color = Color.WHITE alpha = 40 // Reduced from 80 for more subtlety 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) } // 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 } // Pre-allocate paint objects to avoid GC private val tmpPaint = Paint() // 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 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 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.3f // Reduced from 0.5f for more sensitive horizontal movement private val directionLockThreshold = 1.0f // Reduced from 1.5f to make direction locking less aggressive private val isStrictDirectionLock = true // Re-enabled strict direction locking to prevent diagonal inputs private val minHardDropDistance = 2.5f // Increased from 1.5f to require more deliberate hard drops private val minHoldDistance = 2.0f // Minimum distance (in blocks) for hold gesture private val maxSoftDropDistance = 1.5f // Maximum distance for soft drop before considering hard drop // Block skin private var currentBlockSkin: String = "block_skin_1" private val blockSkinPaints = mutableMapOf() private var currentThemeColor = Color.WHITE private enum class Direction { HORIZONTAL, VERTICAL } // 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 // Animation state private var pulseAnimator: ValueAnimator? = null private var pulseAlpha = 0f private var isPulsing = false private var linesToPulse = mutableListOf() // Track which lines are being cleared private var gameOverAnimator: ValueAnimator? = null private var gameOverAlpha = 0f private var isGameOverAnimating = false // Add new game over animation properties private var gameOverBlocksY = mutableListOf() private var gameOverBlocksX = mutableListOf() private var gameOverBlocksRotation = mutableListOf() private var gameOverBlocksSpeed = mutableListOf() private var gameOverBlocksSize = mutableListOf() private var gameOverColorTransition = 0f private var gameOverShakeAmount = 0f private var gameOverFinalAlpha = 0.8f private val ghostPaint = Paint().apply { style = Paint.Style.STROKE strokeWidth = 2f color = Color.WHITE alpha = 180 // Increased from 100 for better visibility } private val ghostBackgroundPaint = Paint().apply { style = Paint.Style.FILL color = Color.WHITE alpha = 30 // Very light background for better contrast } private val ghostBorderPaint = Paint().apply { style = Paint.Style.STROKE strokeWidth = 1f color = Color.WHITE alpha = 100 // Subtle border for better definition } init { // Start with paused state pause() // 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" // 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.onLineClear = { lineCount, clearedLines -> Log.d(TAG, "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") startPulseAnimation(lineCount) Log.d(TAG, "Forwarded line clear callback") } catch (e: Exception) { Log.e(TAG, "Error forwarding line clear callback", e) } } // Force hardware acceleration - This is critical for performance setLayerType(LAYER_TYPE_HARDWARE, null) // 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() } 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))) } // 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 style = Paint.Style.FILL } // 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 = false // Pixelated look style = Paint.Style.FILL } // 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) { Log.d("BlockSkin", "Setting block skin from: $currentBlockSkin to: $skinId") // Check if the skin exists in our map if (!blockSkinPaints.containsKey(skinId)) { Log.e("BlockSkin", "Warning: Unknown block skin: $skinId - available skins: ${blockSkinPaints.keys}") // Fall back to default skin if the requested one doesn't exist if (blockSkinPaints.containsKey("block_skin_1")) { currentBlockSkin = "block_skin_1" } } else { // Set the skin currentBlockSkin = skinId } // Save the selection to SharedPreferences val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE) prefs.edit().putString("selected_block_skin", skinId).commit() // Force a refresh of the view invalidate() // Log confirmation Log.d("BlockSkin", "Block skin is now: $currentBlockSkin") } /** * Get the current block skin */ fun getCurrentBlockSkin(): String = currentBlockSkin /** * Start the game */ fun start() { // Reset game over animation state isGameOverAnimating = false gameOverAlpha = 0f gameOverBlocksY.clear() gameOverBlocksX.clear() gameOverBlocksRotation.clear() gameOverBlocksSpeed.clear() gameOverBlocksSize.clear() gameOverColorTransition = 0f gameOverShakeAmount = 0f isPaused = false isRunning = true gameBoard.startGame() // Add this line to ensure a new piece is spawned handler.post(gameLoopRunnable) invalidate() } /** * Pause the game */ fun pause() { isPaused = true handler.removeCallbacks(gameLoopRunnable) invalidate() } /** * Reset the game */ fun reset() { isRunning = false isPaused = true // Reset game over animation state isGameOverAnimating = false gameOverAlpha = 0f gameOverBlocksY.clear() gameOverBlocksX.clear() gameOverBlocksRotation.clear() gameOverBlocksSpeed.clear() gameOverBlocksSize.clear() gameOverColorTransition = 0f gameOverShakeAmount = 0f gameBoard.reset() gameBoard.startGame() // Add this line to ensure a new piece is spawned handler.removeCallbacks(gameLoopRunnable) invalidate() } /** * Update game state (called on game loop) */ private fun update() { if (gameBoard.isGameOver) { // Only trigger game over handling once when transitioning to game over state if (isRunning) { Log.d(TAG, "Game has ended - transitioning to game over state") isRunning = false isPaused = true // Always trigger animation for each game over Log.d(TAG, "Triggering game over animation from update()") startGameOverAnimation() onGameOver?.invoke(gameBoard.score) } return } // Update the game state gameBoard.update() // Update UI with current game state onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) // Force hardware acceleration - Critical for performance 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 // 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: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") } override fun onDraw(canvas: Canvas) { // Set hardware layer type during draw for better performance val wasHardwareAccelerated = isHardwareAccelerated if (!wasHardwareAccelerated) { setLayerType(LAYER_TYPE_HARDWARE, null) } super.onDraw(canvas) // Draw background (already black from theme) // Draw board border glow drawBoardBorder(canvas) // Draw grid (very subtle) drawGrid(canvas) // Draw locked pieces drawLockedBlocks(canvas) if (!gameBoard.isGameOver && isRunning) { // Draw ghost piece (landing preview) drawGhostPiece(canvas) // Draw active piece drawActivePiece(canvas) } // Draw game over effect if animating if (isGameOverAnimating) { // First layer - full screen glow val gameOverPaint = Paint().apply { color = Color.RED // Change to red for more striking game over indication alpha = (230 * gameOverAlpha).toInt() // Increased opacity isAntiAlias = true style = Paint.Style.FILL // Only apply blur if alpha is greater than 0 if (gameOverAlpha > 0) { maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) } } // Apply screen shake if active if (gameOverShakeAmount > 0) { canvas.save() val shakeOffsetX = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 // Doubled for more visible shake val shakeOffsetY = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 canvas.translate(shakeOffsetX.toFloat(), shakeOffsetY.toFloat()) } canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint) // Second layer - color transition effect val gameOverPaint2 = Paint().apply { // Transition from bright red to theme color val transitionColor = if (gameOverColorTransition < 0.5f) { Color.RED } else { val transition = (gameOverColorTransition - 0.5f) * 2f val red = Color.red(currentThemeColor) val green = Color.green(currentThemeColor) val blue = Color.blue(currentThemeColor) Color.argb( (150 * gameOverAlpha).toInt(), // Increased opacity (255 - (255-red) * transition).toInt(), (green * transition).toInt(), // Transition from 0 (red) to theme green (blue * transition).toInt() // Transition from 0 (red) to theme blue ) } color = transitionColor alpha = (150 * gameOverAlpha).toInt() // Increased opacity isAntiAlias = true style = Paint.Style.FILL // Only apply blur if alpha is greater than 0 if (gameOverAlpha > 0) { maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased blur } } canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint2) // Draw "GAME OVER" text if (gameOverAlpha > 0.5f) { val textPaint = Paint().apply { color = Color.WHITE alpha = (255 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt() isAntiAlias = true textSize = blockSize * 1.5f // Reduced from 2f to 1.5f to fit on screen textAlign = Paint.Align.CENTER typeface = android.graphics.Typeface.DEFAULT_BOLD style = Paint.Style.FILL_AND_STROKE strokeWidth = blockSize * 0.08f // Proportionally reduced from 0.1f } // Draw text with glow val glowPaint = Paint(textPaint).apply { maskFilter = BlurMaskFilter(blockSize * 0.4f, BlurMaskFilter.Blur.NORMAL) // Reduced from 0.5f alpha = (200 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt() color = Color.RED strokeWidth = blockSize * 0.15f // Reduced from 0.2f } val xPos = width / 2f val yPos = height / 3f // Measure text width to check if it fits val textWidth = textPaint.measureText("GAME OVER") // If text would still be too wide, scale it down further if (textWidth > width * 0.9f) { val scaleFactor = width * 0.9f / textWidth textPaint.textSize *= scaleFactor glowPaint.textSize *= scaleFactor } canvas.drawText("GAME OVER", xPos, yPos, glowPaint) canvas.drawText("GAME OVER", xPos, yPos, textPaint) } // Draw falling blocks if (gameOverBlocksY.isNotEmpty()) { for (i in gameOverBlocksY.indices) { val x = gameOverBlocksX[i] val y = gameOverBlocksY[i] val rotation = gameOverBlocksRotation[i] val size = gameOverBlocksSize[i] * blockSize // Skip blocks that have fallen off the screen if (y > height) continue // Draw each falling block with rotation canvas.save() canvas.translate(x, y) canvas.rotate(rotation) // Create a pulsing effect for the falling blocks val blockPaint = Paint(blockPaint) blockPaint.alpha = (255 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.7f)).toInt() // Draw block with glow effect val blockGlowPaint = Paint(blockGlowPaint) blockGlowPaint.alpha = (200 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.5f)).toInt() canvas.drawRect(-size/2, -size/2, size/2, size/2, blockGlowPaint) canvas.drawRect(-size/2, -size/2, size/2, size/2, blockPaint) canvas.restore() } } // Reset any transformations from screen shake if (gameOverShakeAmount > 0) { canvas.restore() } } } /** * 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) // 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, borderGlowPaint) // 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 ) // 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 ) } } /** * Draw the grid lines (very subtle) */ private fun drawGrid(canvas: Canvas) { // Save the canvas state to prevent any effects from affecting the grid canvas.save() // 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 ) } // Restore the canvas state canvas.restore() } /** * 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)) { drawBlock(canvas, x, y, false, y in linesToPulse) } } } } /** * 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 // Draw piece regardless of vertical position if (boardX >= 0 && boardX < gameBoard.width) { drawBlock(canvas, boardX, boardY, false, false) } } } } } /** * Draw the ghost piece (landing preview) */ private fun drawGhostPiece(canvas: Canvas) { val piece = gameBoard.getCurrentPiece() ?: return val ghostY = gameBoard.getGhostY() // Draw semi-transparent background for each block 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 if (boardX >= 0 && boardX < gameBoard.width) { val screenX = boardLeft + boardX * blockSize val screenY = boardTop + boardY * blockSize // Draw background canvas.drawRect( screenX + 1f, screenY + 1f, screenX + blockSize - 1f, screenY + blockSize - 1f, ghostBackgroundPaint ) // Draw border canvas.drawRect( screenX + 1f, screenY + 1f, screenX + blockSize - 1f, screenY + blockSize - 1f, ghostBorderPaint ) // Draw outline canvas.drawRect( screenX + 1f, screenY + 1f, screenX + blockSize - 1f, screenY + blockSize - 1f, ghostPaint ) } } } } } /** * Draw a single tetris block at the given grid position */ private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) { val left = boardLeft + x * blockSize val top = boardTop + y * blockSize val right = left + blockSize val bottom = top + blockSize // Save canvas state before drawing block effects canvas.save() // Get the current block skin paint val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!! // Create a clone of the paint to avoid modifying the original val blockPaint = Paint(paint) // Draw block based on current skin when (currentBlockSkin) { "block_skin_1" -> { // Classic // 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 blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE 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) } "block_skin_2" -> { // Neon // 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 glowPaint.color = if (isGhost) Color.argb(10, 255, 0, 255) else Color.parseColor("#FF00FF") glowPaint.alpha = if (isGhost) 10 else 100 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) } "block_skin_3" -> { // Retro // Draw pixelated block with retro effect blockPaint.color = if (isGhost) Color.argb(30, 255, 90, 95) else Color.parseColor("#FF5A5F") blockPaint.alpha = if (isGhost) 30 else 255 // Draw main block canvas.drawRect(left, top, right, bottom, blockPaint) // Draw pixelated highlights val highlightPaint = Paint().apply { color = Color.parseColor("#FF8A8F") isAntiAlias = false style = Paint.Style.FILL } // Top and left highlights canvas.drawRect(left, top, right - 2f, top + 2f, highlightPaint) canvas.drawRect(left, top, left + 2f, bottom - 2f, highlightPaint) // Draw pixelated shadows val shadowPaint = Paint().apply { color = Color.parseColor("#CC4A4F") isAntiAlias = false style = Paint.Style.FILL } // Bottom and right shadows canvas.drawRect(left + 2f, bottom - 2f, right, bottom, shadowPaint) canvas.drawRect(right - 2f, top + 2f, right, bottom - 2f, shadowPaint) } "block_skin_4" -> { // Minimalist // Draw clean, simple block with subtle border blockPaint.color = if (isGhost) Color.argb(30, 0, 0, 0) else Color.BLACK blockPaint.alpha = if (isGhost) 30 else 255 blockPaint.style = Paint.Style.FILL canvas.drawRect(left, top, right, bottom, blockPaint) // Draw subtle border val borderPaint = Paint().apply { color = Color.parseColor("#333333") style = Paint.Style.STROKE strokeWidth = 1f isAntiAlias = true } canvas.drawRect(left, top, right, bottom, borderPaint) } "block_skin_5" -> { // Galaxy // Draw cosmic glow effect blockGlowPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1") blockGlowPaint.maskFilter = BlurMaskFilter(20f, BlurMaskFilter.Blur.OUTER) canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, blockGlowPaint) // Draw main block with gradient val gradient = LinearGradient( left, top, right, bottom, Color.parseColor("#66FCF1"), Color.parseColor("#45B7AF"), Shader.TileMode.CLAMP ) blockPaint.shader = gradient blockPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1") blockPaint.alpha = if (isGhost) 30 else 255 blockPaint.style = Paint.Style.FILL canvas.drawRect(left, top, right, bottom, blockPaint) // Draw star-like sparkles if (!isGhost) { val sparklePaint = Paint().apply { color = Color.WHITE style = Paint.Style.FILL isAntiAlias = true maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL) } // Add small white dots for sparkle effect canvas.drawCircle(left + 4f, top + 4f, 1f, sparklePaint) canvas.drawCircle(right - 4f, bottom - 4f, 1f, sparklePaint) } } } // 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() } /** * 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 } // 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 -> { startX = event.x startY = event.y lastTouchX = event.x lastTouchY = event.y lastTapTime = currentTime // Set the tap time when touch starts // Reset direction lock lockedDirection = null } MotionEvent.ACTION_MOVE -> { val deltaX = event.x - lastTouchX val deltaY = event.y - lastTouchY // Check if we should lock direction if (lockedDirection == null) { val absDeltaX = abs(deltaX) val absDeltaY = abs(deltaY) if (absDeltaX > blockSize * directionLockThreshold || absDeltaY > blockSize * directionLockThreshold) { // Lock to the dominant direction lockedDirection = if (absDeltaX > absDeltaY) { Direction.HORIZONTAL } else { Direction.VERTICAL } } } // 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) { gameBoard.softDrop() lastTouchY = event.y if (currentTime - lastMoveTime >= moveCooldown) { gameHaptics?.vibrateForPieceMove() lastMoveTime = currentTime } invalidate() } } null -> { // No direction lock yet, don't process movement } } } MotionEvent.ACTION_UP -> { val deltaX = event.x - startX val deltaY = event.y - startY val moveTime = currentTime - lastTapTime // Handle taps for rotation if (moveTime < minTapTime * 1.5 && abs(deltaY) < maxTapMovement * 1.5 && abs(deltaX) < maxTapMovement * 1.5) { if (currentTime - lastRotationTime >= rotationCooldown) { gameBoard.rotate() lastRotationTime = currentTime gameHaptics?.vibrateForPieceMove() invalidate() } return true } // Handle gestures // Check for hold gesture (swipe up) if (deltaY < -blockSize * minHoldDistance && abs(deltaX) / abs(deltaY) < 0.5f) { if (currentTime - lastHoldTime >= holdCooldown) { gameBoard.holdPiece() lastHoldTime = currentTime gameHaptics?.vibrateForPieceMove() invalidate() } } // Check for hard drop (must be faster and longer than soft drop) else if (deltaY > blockSize * minHardDropDistance && abs(deltaX) / abs(deltaY) < 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) { if (currentTime - lastHardDropTime >= hardDropCooldown) { gameBoard.hardDrop() lastHardDropTime = currentTime invalidate() } } // Check for soft drop (slower and shorter than hard drop) else if (deltaY > blockSize * minMovementThreshold && deltaY < blockSize * maxSoftDropDistance && (deltaY / moveTime) * 1000 < minSwipeVelocity) { gameBoard.softDrop() invalidate() } // Reset direction lock lockedDirection = null } } 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 /** * Get the next piece that will be spawned */ fun getNextPiece(): Tetromino? { return gameBoard.getNextPiece() } /** * Get the game board instance */ fun getGameBoard(): GameBoard = gameBoard /** * Clean up resources when view is detached */ override fun onDetachedFromWindow() { super.onDetachedFromWindow() handler.removeCallbacks(gameLoopRunnable) } /** * Set the game board for this view */ fun setGameBoard(board: GameBoard) { gameBoard = board // Reconnect callbacks to the new board gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onLineClear = { lineCount, clearedLines -> Log.d(TAG, "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") startPulseAnimation(lineCount) Log.d(TAG, "Forwarded line clear callback") } catch (e: Exception) { Log.e(TAG, "Error forwarding line clear callback", e) } } 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 // Restart the game loop immediately handler.removeCallbacks(gameLoopRunnable) handler.post(gameLoopRunnable) // Force an update to ensure pieces move immediately update() invalidate() } /** * Start the pulse animation for line clear */ private fun startPulseAnimation(lineCount: Int) { Log.d(TAG, "Starting pulse animation for $lineCount lines") // 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() Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha") } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { isPulsing = false pulseAlpha = 0f linesToPulse.clear() invalidate() Log.d(TAG, "Pulse animation ended") } }) } pulseAnimator?.start() } /** * Set the theme color for the game view */ fun setThemeColor(color: Int) { currentThemeColor = color blockPaint.color = color ghostBlockPaint.color = color glowPaint.color = color blockGlowPaint.color = color borderGlowPaint.color = color pulsePaint.color = color invalidate() } /** * Set the background color for the game view */ override fun setBackgroundColor(color: Int) { super.setBackgroundColor(color) invalidate() } /** * Start the game over animation */ fun startGameOverAnimation() { Log.d(TAG, "Starting game over animation") // Check if game over already showing if (isGameOverAnimating && gameOverAlpha > 0.5f) { Log.d(TAG, "Game over animation already active - skipping") return } // Cancel any existing animations pulseAnimator?.cancel() gameOverAnimator?.cancel() // Trigger haptic feedback gameHaptics?.vibrateForGameOver() // Force immediate visual feedback isGameOverAnimating = true gameOverAlpha = 0.3f invalidate() // Generate falling blocks based on current board state generateGameOverBlocks() // Create new game over animation gameOverAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 3000L // Increased from 2000L (2 seconds) to 3000L (3 seconds) interpolator = LinearInterpolator() addUpdateListener { animation -> val progress = animation.animatedValue as Float // Main alpha transition for overlay gameOverAlpha = when { progress < 0.2f -> progress * 5f // Quick fade in (first 20% of animation) else -> 1f // Hold at full opacity } // Color transition effect (start after 40% of animation) gameOverColorTransition = when { progress < 0.4f -> 0f progress < 0.8f -> (progress - 0.4f) * 2.5f // Transition during 40%-80% else -> 1f } // Screen shake effect (strongest at beginning, fades out) gameOverShakeAmount = when { progress < 0.3f -> progress * 3.33f // Ramp up progress < 0.6f -> 1f - (progress - 0.3f) * 3.33f // Ramp down else -> 0f // No shake } // Update falling blocks updateGameOverBlocks() isGameOverAnimating = true invalidate() Log.d(TAG, "Game over animation update: alpha = $gameOverAlpha, progress = $progress") } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationEnd(animation: android.animation.Animator) { Log.d(TAG, "Game over animation ended - Final alpha: $gameOverFinalAlpha") isGameOverAnimating = true // Keep true to maintain visibility gameOverAlpha = gameOverFinalAlpha // Keep at 80% opacity invalidate() } }) } gameOverAnimator?.start() } /** * Generate falling blocks for the game over animation based on current board state */ private fun generateGameOverBlocks() { // Clear existing blocks gameOverBlocksY.clear() gameOverBlocksX.clear() gameOverBlocksRotation.clear() gameOverBlocksSpeed.clear() gameOverBlocksSize.clear() // Generate 30-40 blocks across the board val numBlocks = (30 + Math.random() * 10).toInt() for (i in 0 until numBlocks) { // Start positions - distribute across the board width but clustered near the top gameOverBlocksX.add(boardLeft + (Math.random() * gameBoard.width * blockSize).toFloat()) gameOverBlocksY.add((boardTop - blockSize * 2 + Math.random() * height * 0.3f).toFloat()) // Random rotation gameOverBlocksRotation.add((Math.random() * 360).toFloat()) // Random fall speed (some faster, some slower) gameOverBlocksSpeed.add((5 + Math.random() * 15).toFloat()) // Slightly varied block sizes gameOverBlocksSize.add((0.8f + Math.random() * 0.4f).toFloat()) } } /** * Update the position of falling blocks in the game over animation */ private fun updateGameOverBlocks() { for (i in gameOverBlocksY.indices) { // Update Y position based on speed gameOverBlocksY[i] += gameOverBlocksSpeed[i] // Update rotation gameOverBlocksRotation[i] += gameOverBlocksSpeed[i] * 0.5f // Accelerate falling gameOverBlocksSpeed[i] *= 1.03f } } /** * Check if the game is active (running, not paused, not game over) */ fun isActive(): Boolean { return isRunning && !isPaused && !gameBoard.isGameOver } /** * Move the current piece left (for gamepad/keyboard support) */ fun moveLeft() { if (!isActive()) return gameBoard.moveLeft() gameHaptics?.vibrateForPieceMove() invalidate() } /** * Move the current piece right (for gamepad/keyboard support) */ fun moveRight() { if (!isActive()) return gameBoard.moveRight() gameHaptics?.vibrateForPieceMove() invalidate() } /** * Rotate the current piece (for gamepad/keyboard support) */ fun rotate() { if (!isActive()) return gameBoard.rotate() gameHaptics?.vibrateForPieceMove() invalidate() } /** * Rotate the current piece counterclockwise (for gamepad/keyboard support) */ fun rotateCounterClockwise() { if (!isActive()) return gameBoard.rotateCounterClockwise() gameHaptics?.vibrateForPieceMove() invalidate() } /** * Perform a soft drop (move down faster) (for gamepad/keyboard support) */ fun softDrop() { if (!isActive()) return gameBoard.softDrop() gameHaptics?.vibrateForPieceMove() invalidate() } /** * Perform a hard drop (instant drop) (for gamepad/keyboard support) */ fun hardDrop() { if (!isActive()) return gameBoard.hardDrop() // Hard drop haptic feedback is handled by the game board via onPieceLock invalidate() } /** * Hold the current piece (for gamepad/keyboard support) */ fun holdPiece() { if (!isActive()) return gameBoard.holdPiece() gameHaptics?.vibrateForPieceMove() invalidate() } }