2025-03-26 12:44:00 -04:00
|
|
|
package com.mintris.game
|
|
|
|
|
|
|
|
import android.animation.ValueAnimator
|
|
|
|
import android.content.Context
|
2025-03-28 20:17:44 -04:00
|
|
|
import android.graphics.BlurMaskFilter
|
2025-03-26 12:44:00 -04:00
|
|
|
import android.graphics.Canvas
|
|
|
|
import android.graphics.Color
|
2025-03-28 20:17:44 -04:00
|
|
|
import android.graphics.LinearGradient
|
2025-03-26 12:44:00 -04:00
|
|
|
import android.graphics.Paint
|
|
|
|
import android.graphics.Rect
|
|
|
|
import android.graphics.RectF
|
2025-03-28 20:17:44 -04:00
|
|
|
import android.graphics.Shader
|
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
|
|
|
|
}
|
|
|
|
|
2025-03-30 19:14:16 -04:00
|
|
|
private val borderGlowPaint = Paint().apply {
|
|
|
|
color = Color.WHITE
|
|
|
|
alpha = 60
|
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.STROKE
|
|
|
|
strokeWidth = 2f
|
|
|
|
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
|
|
|
}
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
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
|
|
|
}
|
|
|
|
|
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-29 23:20:14 -04:00
|
|
|
private val maxTapMovement = 30f // Increased from 20f to 30f for more lenient tap detection
|
2025-03-26 12:44:00 -04:00
|
|
|
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-29 23:20:14 -04:00
|
|
|
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
|
2025-03-27 20:47:20 -04:00
|
|
|
private var lockedDirection: Direction? = null // Track the locked movement direction
|
2025-03-30 15:45:19 -04:00
|
|
|
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
|
2025-03-29 23:20:14 -04:00
|
|
|
private val isStrictDirectionLock = true // Re-enabled strict direction locking to prevent diagonal inputs
|
2025-03-30 15:45:19 -04:00
|
|
|
private val minHardDropDistance = 2.5f // Increased from 1.5f to require more deliberate hard drops
|
2025-03-29 23:20:14 -04:00
|
|
|
private val minHoldDistance = 2.0f // Minimum distance (in blocks) for hold gesture
|
2025-03-30 15:45:19 -04:00
|
|
|
private val maxSoftDropDistance = 1.5f // Maximum distance for soft drop before considering hard drop
|
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-29 01:59:14 -04:00
|
|
|
private var currentThemeColor = Color.WHITE
|
2025-03-28 15:45:20 -04:00
|
|
|
|
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-30 19:02:27 -04:00
|
|
|
private var gameOverAnimator: ValueAnimator? = null
|
|
|
|
private var gameOverAlpha = 0f
|
|
|
|
private var isGameOverAnimating = false
|
2025-03-30 19:39:35 -04:00
|
|
|
// Add new game over animation properties
|
|
|
|
private var gameOverBlocksY = mutableListOf<Float>()
|
|
|
|
private var gameOverBlocksX = mutableListOf<Float>()
|
|
|
|
private var gameOverBlocksRotation = mutableListOf<Float>()
|
|
|
|
private var gameOverBlocksSpeed = mutableListOf<Float>()
|
|
|
|
private var gameOverBlocksSize = mutableListOf<Float>()
|
|
|
|
private var gameOverColorTransition = 0f
|
|
|
|
private var gameOverShakeAmount = 0f
|
|
|
|
private var gameOverFinalAlpha = 0.8f
|
2025-03-27 01:54:55 -04:00
|
|
|
|
2025-03-30 15:54:36 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
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
|
2025-03-28 20:17:44 -04:00
|
|
|
style = Paint.Style.FILL
|
2025-03-28 15:45:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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")
|
2025-03-28 20:17:44 -04:00
|
|
|
isAntiAlias = false // Pixelated look
|
|
|
|
style = Paint.Style.FILL
|
2025-03-28 15:45:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2025-03-31 17:07:28 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
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-31 17:07:28 -04:00
|
|
|
|
|
|
|
// Force a refresh of the view
|
2025-03-28 15:45:20 -04:00
|
|
|
invalidate()
|
2025-03-31 17:07:28 -04:00
|
|
|
|
|
|
|
// Log confirmation
|
|
|
|
Log.d("BlockSkin", "Block skin is now: $currentBlockSkin")
|
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-30 19:39:35 -04:00
|
|
|
// Reset game over animation state
|
|
|
|
isGameOverAnimating = false
|
|
|
|
gameOverAlpha = 0f
|
|
|
|
gameOverBlocksY.clear()
|
|
|
|
gameOverBlocksX.clear()
|
|
|
|
gameOverBlocksRotation.clear()
|
|
|
|
gameOverBlocksSpeed.clear()
|
|
|
|
gameOverBlocksSize.clear()
|
|
|
|
gameOverColorTransition = 0f
|
|
|
|
gameOverShakeAmount = 0f
|
|
|
|
|
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
|
2025-03-30 19:39:35 -04:00
|
|
|
|
|
|
|
// Reset game over animation state
|
|
|
|
isGameOverAnimating = false
|
|
|
|
gameOverAlpha = 0f
|
|
|
|
gameOverBlocksY.clear()
|
|
|
|
gameOverBlocksX.clear()
|
|
|
|
gameOverBlocksRotation.clear()
|
|
|
|
gameOverBlocksSpeed.clear()
|
|
|
|
gameOverBlocksSize.clear()
|
|
|
|
gameOverColorTransition = 0f
|
|
|
|
gameOverShakeAmount = 0f
|
|
|
|
|
2025-03-26 12:44:00 -04:00
|
|
|
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) {
|
2025-03-30 19:39:35 -04:00
|
|
|
// 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)
|
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
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
|
|
|
// 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)
|
|
|
|
}
|
2025-03-30 19:02:27 -04:00
|
|
|
|
|
|
|
// Draw game over effect if animating
|
|
|
|
if (isGameOverAnimating) {
|
2025-03-30 19:39:35 -04:00
|
|
|
// First layer - full screen glow
|
2025-03-30 19:02:27 -04:00
|
|
|
val gameOverPaint = Paint().apply {
|
2025-03-30 19:39:35 -04:00
|
|
|
color = Color.RED // Change to red for more striking game over indication
|
|
|
|
alpha = (230 * gameOverAlpha).toInt() // Increased opacity
|
2025-03-30 19:02:27 -04:00
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.FILL
|
2025-03-30 19:14:16 -04:00
|
|
|
// Only apply blur if alpha is greater than 0
|
|
|
|
if (gameOverAlpha > 0) {
|
2025-03-30 19:39:35 -04:00
|
|
|
maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER)
|
2025-03-30 19:14:16 -04:00
|
|
|
}
|
2025-03-30 19:02:27 -04:00
|
|
|
}
|
2025-03-30 19:39:35 -04:00
|
|
|
|
|
|
|
// 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())
|
|
|
|
}
|
|
|
|
|
2025-03-30 19:02:27 -04:00
|
|
|
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint)
|
2025-03-30 19:14:16 -04:00
|
|
|
|
2025-03-30 19:39:35 -04:00
|
|
|
// Second layer - color transition effect
|
2025-03-30 19:14:16 -04:00
|
|
|
val gameOverPaint2 = Paint().apply {
|
2025-03-30 19:39:35 -04:00
|
|
|
// 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
|
2025-03-30 19:14:16 -04:00
|
|
|
isAntiAlias = true
|
|
|
|
style = Paint.Style.FILL
|
|
|
|
// Only apply blur if alpha is greater than 0
|
|
|
|
if (gameOverAlpha > 0) {
|
2025-03-30 19:39:35 -04:00
|
|
|
maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased blur
|
2025-03-30 19:14:16 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint2)
|
2025-03-30 19:39:35 -04:00
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|
2025-03-30 19:02:27 -04:00
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
|
2025-03-30 15:54:36 -04:00
|
|
|
// Draw semi-transparent background for each block
|
2025-03-26 12:44:00 -04:00
|
|
|
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
|
|
|
if (boardX >= 0 && boardX < gameBoard.width) {
|
2025-03-30 15:54:36 -04:00
|
|
|
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
|
|
|
|
)
|
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)
|
|
|
|
|
2025-03-28 20:17:44 -04:00
|
|
|
// 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)
|
2025-03-28 16:11:30 -04:00
|
|
|
}
|
2025-03-28 20:17:44 -04:00
|
|
|
"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)
|
|
|
|
}
|
2025-03-28 16:11:30 -04:00
|
|
|
}
|
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 -> {
|
|
|
|
startX = event.x
|
|
|
|
startY = event.y
|
|
|
|
lastTouchX = event.x
|
|
|
|
lastTouchY = event.y
|
2025-03-29 23:20:14 -04:00
|
|
|
lastTapTime = currentTime // Set the tap time when touch starts
|
2025-03-26 12:44:00 -04:00
|
|
|
|
2025-03-29 23:20:14 -04:00
|
|
|
// Reset direction lock
|
|
|
|
lockedDirection = null
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
|
|
val deltaX = event.x - lastTouchX
|
|
|
|
val deltaY = event.y - lastTouchY
|
|
|
|
|
2025-03-29 23:20:14 -04:00
|
|
|
// Check if we should lock direction
|
2025-03-27 20:47:20 -04:00
|
|
|
if (lockedDirection == null) {
|
|
|
|
val absDeltaX = abs(deltaX)
|
|
|
|
val absDeltaY = abs(deltaY)
|
|
|
|
|
2025-03-29 23:20:14 -04:00
|
|
|
if (absDeltaX > blockSize * directionLockThreshold ||
|
|
|
|
absDeltaY > blockSize * directionLockThreshold) {
|
|
|
|
// Lock to the dominant direction
|
|
|
|
lockedDirection = if (absDeltaX > absDeltaY) {
|
|
|
|
Direction.HORIZONTAL
|
|
|
|
} else {
|
|
|
|
Direction.VERTICAL
|
2025-03-27 20:47:20 -04:00
|
|
|
}
|
2025-03-26 16:09:03 -04:00
|
|
|
}
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
2025-03-31 04:05:50 -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
|
|
|
|
}
|
2025-03-27 20:47:20 -04:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
2025-03-31 04:05:50 -04:00
|
|
|
Direction.VERTICAL -> {
|
|
|
|
if (deltaY > blockSize * minMovementThreshold) {
|
|
|
|
gameBoard.softDrop()
|
|
|
|
lastTouchY = event.y
|
|
|
|
if (currentTime - lastMoveTime >= moveCooldown) {
|
|
|
|
gameHaptics?.vibrateForPieceMove()
|
|
|
|
lastMoveTime = currentTime
|
|
|
|
}
|
2025-03-27 20:47:20 -04:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
2025-03-31 04:05:50 -04:00
|
|
|
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 -> {
|
|
|
|
val deltaX = event.x - startX
|
2025-03-29 23:20:14 -04:00
|
|
|
val deltaY = event.y - startY
|
|
|
|
val moveTime = currentTime - lastTapTime
|
2025-03-28 18:59:13 -04:00
|
|
|
|
2025-03-31 04:05:50 -04:00
|
|
|
// Handle taps for rotation
|
2025-03-31 03:46:05 -04:00
|
|
|
if (moveTime < minTapTime * 1.5 &&
|
|
|
|
abs(deltaY) < maxTapMovement * 1.5 &&
|
|
|
|
abs(deltaX) < maxTapMovement * 1.5) {
|
|
|
|
|
2025-03-31 04:05:50 -04:00
|
|
|
if (currentTime - lastRotationTime >= rotationCooldown) {
|
2025-03-31 03:46:05 -04:00
|
|
|
gameBoard.rotate()
|
|
|
|
lastRotationTime = currentTime
|
|
|
|
gameHaptics?.vibrateForPieceMove()
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2025-03-31 04:05:50 -04:00
|
|
|
// Handle gestures
|
2025-03-29 23:20:14 -04:00
|
|
|
// Check for hold gesture (swipe up)
|
|
|
|
if (deltaY < -blockSize * minHoldDistance &&
|
|
|
|
abs(deltaX) / abs(deltaY) < 0.5f) {
|
2025-03-31 04:05:50 -04:00
|
|
|
if (currentTime - lastHoldTime >= holdCooldown) {
|
2025-03-29 23:20:14 -04:00
|
|
|
gameBoard.holdPiece()
|
|
|
|
lastHoldTime = currentTime
|
|
|
|
gameHaptics?.vibrateForPieceMove()
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
}
|
2025-03-30 15:45:19 -04:00
|
|
|
// Check for hard drop (must be faster and longer than soft drop)
|
2025-03-29 23:20:14 -04:00
|
|
|
else if (deltaY > blockSize * minHardDropDistance &&
|
2025-03-30 15:45:19 -04:00
|
|
|
abs(deltaX) / abs(deltaY) < 0.5f &&
|
|
|
|
(deltaY / moveTime) * 1000 > minSwipeVelocity) {
|
2025-03-31 04:05:50 -04:00
|
|
|
if (currentTime - lastHardDropTime >= hardDropCooldown) {
|
2025-03-28 18:59:13 -04:00
|
|
|
gameBoard.hardDrop()
|
2025-03-29 23:20:14 -04:00
|
|
|
lastHardDropTime = currentTime
|
2025-03-28 18:59:13 -04:00
|
|
|
invalidate()
|
|
|
|
}
|
2025-03-29 23:20:14 -04:00
|
|
|
}
|
2025-03-30 15:45:19 -04:00
|
|
|
// 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()
|
|
|
|
}
|
2025-03-27 20:47:20 -04:00
|
|
|
|
|
|
|
// Reset direction lock
|
|
|
|
lockedDirection = null
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
}
|
2025-03-28 20:41:03 -04:00
|
|
|
|
|
|
|
return true
|
2025-03-26 12:44:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
2025-03-29 23:20:14 -04:00
|
|
|
/**
|
|
|
|
* Get the game board instance
|
|
|
|
*/
|
|
|
|
fun getGameBoard(): GameBoard = gameBoard
|
|
|
|
|
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-29 01:59:14 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
}
|
2025-03-30 19:02:27 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the game over animation
|
|
|
|
*/
|
|
|
|
fun startGameOverAnimation() {
|
|
|
|
Log.d(TAG, "Starting game over animation")
|
|
|
|
|
2025-03-30 19:39:35 -04:00
|
|
|
// Check if game over already showing
|
|
|
|
if (isGameOverAnimating && gameOverAlpha > 0.5f) {
|
|
|
|
Log.d(TAG, "Game over animation already active - skipping")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-03-30 19:02:27 -04:00
|
|
|
// Cancel any existing animations
|
|
|
|
pulseAnimator?.cancel()
|
|
|
|
gameOverAnimator?.cancel()
|
|
|
|
|
|
|
|
// Trigger haptic feedback
|
|
|
|
gameHaptics?.vibrateForGameOver()
|
|
|
|
|
2025-03-30 19:39:35 -04:00
|
|
|
// Force immediate visual feedback
|
|
|
|
isGameOverAnimating = true
|
|
|
|
gameOverAlpha = 0.3f
|
|
|
|
invalidate()
|
|
|
|
|
|
|
|
// Generate falling blocks based on current board state
|
|
|
|
generateGameOverBlocks()
|
|
|
|
|
2025-03-30 19:02:27 -04:00
|
|
|
// Create new game over animation
|
2025-03-30 19:39:35 -04:00
|
|
|
gameOverAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
|
|
duration = 3000L // Increased from 2000L (2 seconds) to 3000L (3 seconds)
|
2025-03-30 19:02:27 -04:00
|
|
|
interpolator = LinearInterpolator()
|
|
|
|
|
|
|
|
addUpdateListener { animation ->
|
2025-03-30 19:39:35 -04:00
|
|
|
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()
|
|
|
|
|
2025-03-30 19:02:27 -04:00
|
|
|
isGameOverAnimating = true
|
|
|
|
invalidate()
|
2025-03-30 19:39:35 -04:00
|
|
|
Log.d(TAG, "Game over animation update: alpha = $gameOverAlpha, progress = $progress")
|
2025-03-30 19:02:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
|
|
override fun onAnimationEnd(animation: android.animation.Animator) {
|
2025-03-30 19:39:35 -04:00
|
|
|
Log.d(TAG, "Game over animation ended - Final alpha: $gameOverFinalAlpha")
|
|
|
|
isGameOverAnimating = true // Keep true to maintain visibility
|
|
|
|
gameOverAlpha = gameOverFinalAlpha // Keep at 80% opacity
|
2025-03-30 19:02:27 -04:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
gameOverAnimator?.start()
|
|
|
|
}
|
2025-03-30 19:39:35 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
}
|
2025-03-31 04:52:11 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
}
|
2025-03-31 15:20:54 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Rotate the current piece counterclockwise (for gamepad/keyboard support)
|
|
|
|
*/
|
|
|
|
fun rotateCounterClockwise() {
|
|
|
|
if (!isActive()) return
|
|
|
|
gameBoard.rotateCounterClockwise()
|
|
|
|
gameHaptics?.vibrateForPieceMove()
|
|
|
|
invalidate()
|
|
|
|
}
|
2025-03-31 04:52:11 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
}
|
2025-03-26 19:39:14 -04:00
|
|
|
}
|