Optimize line clear animation for smoother gameplay: - Reduce animation duration to 100ms - Improve animation smoothness with better scaling and fade effects - Eliminate initial animation delay - Add subtle glow effects for better visual feedback

This commit is contained in:
cmclark00 2025-03-26 16:09:03 -04:00
parent f4e5a9b651
commit fabb2742da
7 changed files with 344 additions and 263 deletions

View file

@ -16,58 +16,43 @@ import com.mintris.game.GameHaptics
import com.mintris.game.GameView import com.mintris.game.GameView
import com.mintris.game.NextPieceView import com.mintris.game.NextPieceView
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import com.mintris.model.GameBoard
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
// UI components // UI components
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var gameView: GameView private lateinit var gameView: GameView
private lateinit var scoreText: TextView
private lateinit var levelText: TextView
private lateinit var linesText: TextView
private lateinit var gameOverContainer: LinearLayout
private lateinit var pauseContainer: LinearLayout
private lateinit var playAgainButton: Button
private lateinit var resumeButton: Button
private lateinit var settingsButton: Button
private lateinit var finalScoreText: TextView
private lateinit var nextPieceView: NextPieceView
private lateinit var levelDownButton: Button
private lateinit var levelUpButton: Button
private lateinit var selectedLevelText: TextView
private lateinit var gameHaptics: GameHaptics private lateinit var gameHaptics: GameHaptics
private lateinit var gameBoard: GameBoard
// Game state // Game state
private var isSoundEnabled = true private var isSoundEnabled = true
private var selectedLevel = 1 private var selectedLevel = 1
private val maxLevel = 10 private val maxLevel = 20
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// Initialize game haptics // Initialize game components
gameBoard = GameBoard()
gameHaptics = GameHaptics(this) gameHaptics = GameHaptics(this)
gameView = binding.gameView
// Set up game view // Set up game view
gameView = binding.gameView gameView.setGameBoard(gameBoard)
scoreText = binding.scoreText gameView.setHaptics(gameHaptics)
levelText = binding.levelText
linesText = binding.linesText
gameOverContainer = binding.gameOverContainer
pauseContainer = binding.pauseContainer
playAgainButton = binding.playAgainButton
resumeButton = binding.resumeButton
settingsButton = binding.settingsButton
finalScoreText = binding.finalScoreText
nextPieceView = binding.nextPieceView
levelDownButton = binding.levelDownButton
levelUpButton = binding.levelUpButton
selectedLevelText = binding.selectedLevelText
// Connect the next piece view to the game view // Set up next piece preview
nextPieceView.setGameView(gameView) binding.nextPieceView.setGameView(gameView)
gameBoard.onNextPieceChanged = {
binding.nextPieceView.invalidate()
}
// Start game immediately
startGame()
// Set up callbacks // Set up callbacks
gameView.onGameStateChanged = { score, level, lines -> gameView.onGameStateChanged = { score, level, lines ->
@ -99,38 +84,51 @@ class MainActivity : AppCompatActivity() {
} }
// Set up button click listeners with haptic feedback // Set up button click listeners with haptic feedback
playAgainButton.setOnClickListener { binding.playAgainButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hideGameOver() hideGameOver()
gameView.reset() gameView.reset()
setGameLevel(selectedLevel)
gameView.start() gameView.start()
} }
resumeButton.setOnClickListener { binding.resumeButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hidePauseMenu() hidePauseMenu()
gameView.start() gameView.start()
} }
settingsButton.setOnClickListener { binding.settingsButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
toggleSound() toggleSound()
} }
// Set up level selector with haptic feedback // Set up pause menu buttons
levelDownButton.setOnClickListener { binding.pauseStartButton.setOnClickListener {
if (selectedLevel > 1) {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
selectedLevel-- hidePauseMenu()
gameView.reset()
gameView.start()
}
binding.pauseRestartButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hidePauseMenu()
gameView.reset()
gameView.start()
}
binding.pauseLevelUpButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
if (selectedLevel < maxLevel) {
selectedLevel++
updateLevelSelector() updateLevelSelector()
} }
} }
levelUpButton.setOnClickListener { binding.pauseLevelDownButton.setOnClickListener {
if (selectedLevel < maxLevel) {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
selectedLevel++ if (selectedLevel > 1) {
selectedLevel--
updateLevelSelector() updateLevelSelector()
} }
} }
@ -138,57 +136,30 @@ class MainActivity : AppCompatActivity() {
// Initialize level selector // Initialize level selector
updateLevelSelector() updateLevelSelector()
// Start game when clicking the screen initially
setupTouchToStart()
// Start with the game paused
gameView.pause()
// Enable edge-to-edge display // Enable edge-to-edge display
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false) window.setDecorFitsSystemWindows(false)
} }
} }
/**
* Set up touch-to-start behavior for initial screen
*/
private fun setupTouchToStart() {
val touchToStart = View.OnClickListener {
if (gameView.isGameOver()) {
hideGameOver()
gameView.reset()
gameView.start()
} else if (pauseContainer.visibility == View.VISIBLE) {
hidePauseMenu()
gameView.start()
} else {
gameView.start()
}
}
// Add the click listener to the game view
gameView.setOnClickListener(touchToStart)
}
/** /**
* Update UI with current game state * Update UI with current game state
*/ */
private fun updateUI(score: Int, level: Int, lines: Int) { private fun updateUI(score: Int, level: Int, lines: Int) {
scoreText.text = score.toString() binding.scoreText.text = score.toString()
levelText.text = level.toString() binding.currentLevelText.text = level.toString()
linesText.text = lines.toString() binding.linesText.text = lines.toString()
// Force redraw of next piece preview // Force redraw of next piece preview
nextPieceView.invalidate() binding.nextPieceView.invalidate()
} }
/** /**
* Show game over screen * Show game over screen
*/ */
private fun showGameOver(score: Int) { private fun showGameOver(score: Int) {
finalScoreText.text = getString(R.string.score) + ": " + score binding.finalScoreText.text = getString(R.string.score) + ": " + score
gameOverContainer.visibility = View.VISIBLE binding.gameOverContainer.visibility = View.VISIBLE
// Vibrate to indicate game over // Vibrate to indicate game over
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK) vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
@ -198,21 +169,23 @@ class MainActivity : AppCompatActivity() {
* Hide game over screen * Hide game over screen
*/ */
private fun hideGameOver() { private fun hideGameOver() {
gameOverContainer.visibility = View.GONE binding.gameOverContainer.visibility = View.GONE
} }
/** /**
* Show pause menu * Show settings menu
*/ */
private fun showPauseMenu() { private fun showPauseMenu() {
pauseContainer.visibility = View.VISIBLE binding.pauseContainer.visibility = View.VISIBLE
binding.pauseStartButton.visibility = View.VISIBLE
binding.resumeButton.visibility = View.GONE
} }
/** /**
* Hide pause menu * Hide settings menu
*/ */
private fun hidePauseMenu() { private fun hidePauseMenu() {
pauseContainer.visibility = View.GONE binding.pauseContainer.visibility = View.GONE
} }
/** /**
@ -220,7 +193,7 @@ class MainActivity : AppCompatActivity() {
*/ */
private fun toggleSound() { private fun toggleSound() {
isSoundEnabled = !isSoundEnabled isSoundEnabled = !isSoundEnabled
settingsButton.text = getString( binding.settingsButton.text = getString(
if (isSoundEnabled) R.string.sound_on else R.string.sound_off if (isSoundEnabled) R.string.sound_on else R.string.sound_off
) )
@ -228,6 +201,14 @@ class MainActivity : AppCompatActivity() {
vibrate(VibrationEffect.EFFECT_CLICK) vibrate(VibrationEffect.EFFECT_CLICK)
} }
/**
* Update the level selector display
*/
private fun updateLevelSelector() {
binding.pauseLevelText.text = selectedLevel.toString()
gameBoard.updateLevel(selectedLevel)
}
/** /**
* Trigger device vibration with predefined effect * Trigger device vibration with predefined effect
*/ */
@ -236,24 +217,18 @@ class MainActivity : AppCompatActivity() {
vibrator.vibrate(VibrationEffect.createPredefined(effectId)) vibrator.vibrate(VibrationEffect.createPredefined(effectId))
} }
/** private fun startGame() {
* Update the level selector display gameView.visibility = View.VISIBLE
*/ gameBoard.startGame()
private fun updateLevelSelector() { gameView.start()
selectedLevelText.text = selectedLevel.toString() hidePauseMenu()
} }
/** private fun restartGame() {
* Set the game level gameBoard.reset()
*/ gameView.visibility = View.VISIBLE
private fun setGameLevel(level: Int) { gameView.start()
gameView.gameBoard.level = level showPauseMenu()
gameView.gameBoard.lines = (level - 1) * 10
gameView.gameBoard.dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
// Update UI
levelText.text = level.toString()
linesText.text = gameView.gameBoard.lines.toString()
} }
override fun onPause() { override fun onPause() {
@ -261,23 +236,32 @@ class MainActivity : AppCompatActivity() {
if (!gameView.isGameOver()) { if (!gameView.isGameOver()) {
gameView.pause() gameView.pause()
showPauseMenu() showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
} }
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
if (gameOverContainer.visibility == View.VISIBLE) { if (binding.gameOverContainer.visibility == View.VISIBLE) {
hideGameOver() hideGameOver()
gameView.reset() gameView.reset()
return return
} }
if (pauseContainer.visibility == View.GONE) { if (binding.pauseContainer.visibility == View.GONE) {
gameView.pause() gameView.pause()
showPauseMenu() showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
} else { } else {
hidePauseMenu() hidePauseMenu()
gameView.start() gameView.start()
} }
} }
override fun onResume() {
super.onResume()
gameView.resume()
}
} }

View file

@ -7,6 +7,7 @@ import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -33,12 +34,16 @@ class GameView @JvmOverloads constructor(
) : View(context, attrs, defStyleAttr) { ) : View(context, attrs, defStyleAttr) {
// Game board model // Game board model
val gameBoard = GameBoard() private var gameBoard = GameBoard()
private var gameHaptics: GameHaptics? = null
// Game state // Game state
private var isRunning = false private var isRunning = false
private var isPaused = false private var isPaused = false
// Callbacks
var onNextPieceChanged: (() -> Unit)? = null
// Rendering // Rendering
private val blockPaint = Paint().apply { private val blockPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
@ -53,7 +58,7 @@ class GameView @JvmOverloads constructor(
private val gridPaint = Paint().apply { private val gridPaint = Paint().apply {
color = Color.parseColor("#222222") // Very dark gray color = Color.parseColor("#222222") // Very dark gray
alpha = 40 alpha = 20 // Reduced from 40 to be more subtle
isAntiAlias = true isAntiAlias = true
strokeWidth = 1f strokeWidth = 1f
style = Paint.Style.STROKE style = Paint.Style.STROKE
@ -61,19 +66,28 @@ class GameView @JvmOverloads constructor(
private val glowPaint = Paint().apply { private val glowPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
alpha = 80 alpha = 40 // Reduced from 80 for more subtlety
isAntiAlias = true isAntiAlias = true
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 2f 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)
} }
private val borderGlowPaint = Paint().apply { private val borderGlowPaint = Paint().apply {
color = Color.CYAN color = Color.WHITE
alpha = 120 alpha = 60
isAntiAlias = true isAntiAlias = true
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 4f strokeWidth = 2f
setShadowLayer(10f, 0f, 0f, Color.CYAN) maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
} }
private val lineClearPaint = Paint().apply { private val lineClearPaint = Paint().apply {
@ -85,7 +99,7 @@ class GameView @JvmOverloads constructor(
// Animation // Animation
private var lineClearAnimator: ValueAnimator? = null private var lineClearAnimator: ValueAnimator? = null
private var lineClearProgress = 0f private var lineClearProgress = 0f
private val lineClearDuration = 150L // milliseconds private val lineClearDuration = 100L // milliseconds
// Calculate block size based on view dimensions and board size // Calculate block size based on view dimensions and board size
private var blockSize = 0f private var blockSize = 0f
@ -111,10 +125,12 @@ class GameView @JvmOverloads constructor(
private var startY = 0f private var startY = 0f
private var lastTapTime = 0L private var lastTapTime = 0L
private var lastRotationTime = 0L private var lastRotationTime = 0L
private var lastMoveTime = 0L
private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop
private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels) private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
private val minTapTime = 100L // Minimum time for a tap (in milliseconds) private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
// Callback for game events // Callback for game events
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
@ -156,16 +172,12 @@ class GameView @JvmOverloads constructor(
* Start the game * Start the game
*/ */
fun start() { fun start() {
if (isPaused || !isRunning) {
isPaused = false isPaused = false
if (!isRunning) {
isRunning = true isRunning = true
gameBoard.reset() gameBoard.startGame() // Add this line to ensure a new piece is spawned
}
handler.post(gameLoopRunnable) handler.post(gameLoopRunnable)
invalidate() invalidate()
} }
}
/** /**
* Pause the game * Pause the game
@ -184,6 +196,7 @@ class GameView @JvmOverloads constructor(
isRunning = false isRunning = false
isPaused = true isPaused = true
gameBoard.reset() gameBoard.reset()
gameBoard.startGame() // Add this line to ensure a new piece is spawned
handler.removeCallbacks(gameLoopRunnable) handler.removeCallbacks(gameLoopRunnable)
lineClearAnimator?.cancel() lineClearAnimator?.cancel()
invalidate() invalidate()
@ -204,15 +217,13 @@ class GameView @JvmOverloads constructor(
gameBoard.moveDown() gameBoard.moveDown()
// Check if lines need to be cleared and start animation if needed // Check if lines need to be cleared and start animation if needed
if (gameBoard.linesToClear.isNotEmpty() && gameBoard.isLineClearAnimationInProgress) { if (gameBoard.linesToClear.isNotEmpty()) {
// Trigger line clear callback for vibration // Trigger line clear callback for vibration
onLineClear?.invoke(gameBoard.linesToClear.size) onLineClear?.invoke(gameBoard.linesToClear.size)
// Start line clearing animation if not already running // Start line clearing animation immediately
if (lineClearAnimator == null || !lineClearAnimator!!.isRunning) {
startLineClearAnimation() startLineClearAnimation()
} }
}
// Update UI with current game state // Update UI with current game state
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
@ -222,8 +233,13 @@ class GameView @JvmOverloads constructor(
* Start the line clearing animation * Start the line clearing animation
*/ */
private fun startLineClearAnimation() { private fun startLineClearAnimation() {
// Cancel any existing animation
lineClearAnimator?.cancel() lineClearAnimator?.cancel()
// Reset progress
lineClearProgress = 0f
// Create and start new animation immediately
lineClearAnimator = ValueAnimator.ofFloat(0f, 1f).apply { lineClearAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = lineClearDuration duration = lineClearDuration
interpolator = LinearInterpolator() interpolator = LinearInterpolator()
@ -273,9 +289,9 @@ class GameView @JvmOverloads constructor(
height.toFloat() / verticalBlocks height.toFloat() / verticalBlocks
) )
// Center the board within the view // Center horizontally and align to bottom
boardLeft = (width - (blockSize * horizontalBlocks)) / 2 boardLeft = (width - (blockSize * horizontalBlocks)) / 2
boardTop = (height - (blockSize * verticalBlocks)) / 2 boardTop = height - (blockSize * verticalBlocks) // Align to bottom
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
@ -313,12 +329,11 @@ class GameView @JvmOverloads constructor(
private fun drawLineClearAnimation(canvas: Canvas) { private fun drawLineClearAnimation(canvas: Canvas) {
// Draw non-clearing blocks // Draw non-clearing blocks
for (y in 0 until gameBoard.height) { for (y in 0 until gameBoard.height) {
// Skip lines that are being cleared
if (gameBoard.linesToClear.contains(y)) continue if (gameBoard.linesToClear.contains(y)) continue
for (x in 0 until gameBoard.width) { for (x in 0 until gameBoard.width) {
if (gameBoard.isOccupied(x, y)) { if (gameBoard.isOccupied(x, y)) {
drawBlock(canvas, x, y, blockPaint) drawBlock(canvas, x, y, false)
} }
} }
} }
@ -327,8 +342,8 @@ class GameView @JvmOverloads constructor(
for (lineY in gameBoard.linesToClear) { for (lineY in gameBoard.linesToClear) {
for (x in 0 until gameBoard.width) { for (x in 0 until gameBoard.width) {
// Animation effects for all lines simultaneously // Animation effects for all lines simultaneously
val brightness = 255 - (lineClearProgress * 200).toInt() val brightness = 255 - (lineClearProgress * 150).toInt() // Reduced from 200 for smoother fade
val scale = 1.0f - lineClearProgress * 0.5f val scale = 1.0f - lineClearProgress * 0.3f // Reduced from 0.5f for subtler scaling
// Set the paint for the clear animation // Set the paint for the clear animation
lineClearPaint.color = Color.WHITE lineClearPaint.color = Color.WHITE
@ -344,8 +359,8 @@ class GameView @JvmOverloads constructor(
val rect = RectF(left, top, right, bottom) val rect = RectF(left, top, right, bottom)
canvas.drawRect(rect, lineClearPaint) canvas.drawRect(rect, lineClearPaint)
// Add a glow effect // Add a more subtle glow effect
lineClearPaint.setShadowLayer(10f * (1f - lineClearProgress), 0f, 0f, Color.WHITE) lineClearPaint.setShadowLayer(8f * (1f - lineClearProgress), 0f, 0f, Color.WHITE)
canvas.drawRect(rect, lineClearPaint) canvas.drawRect(rect, lineClearPaint)
} }
} }
@ -396,7 +411,7 @@ class GameView @JvmOverloads constructor(
for (y in 0 until gameBoard.height) { for (y in 0 until gameBoard.height) {
for (x in 0 until gameBoard.width) { for (x in 0 until gameBoard.width) {
if (gameBoard.isOccupied(x, y)) { if (gameBoard.isOccupied(x, y)) {
drawBlock(canvas, x, y, blockPaint) drawBlock(canvas, x, y, false)
} }
} }
} }
@ -417,7 +432,7 @@ class GameView @JvmOverloads constructor(
// Only draw if within bounds and visible on screen // Only draw if within bounds and visible on screen
if (boardY >= 0 && boardY < gameBoard.height && if (boardY >= 0 && boardY < gameBoard.height &&
boardX >= 0 && boardX < gameBoard.width) { boardX >= 0 && boardX < gameBoard.width) {
drawBlock(canvas, boardX, boardY, blockPaint) drawBlock(canvas, boardX, boardY, false)
} }
} }
} }
@ -440,7 +455,7 @@ class GameView @JvmOverloads constructor(
// Only draw if within bounds and visible on screen // Only draw if within bounds and visible on screen
if (boardY >= 0 && boardY < gameBoard.height && if (boardY >= 0 && boardY < gameBoard.height &&
boardX >= 0 && boardX < gameBoard.width) { boardX >= 0 && boardX < gameBoard.width) {
drawBlock(canvas, boardX, boardY, ghostBlockPaint) drawBlock(canvas, boardX, boardY, true)
} }
} }
} }
@ -450,29 +465,26 @@ class GameView @JvmOverloads constructor(
/** /**
* Draw a single tetris block at the given grid position * Draw a single tetris block at the given grid position
*/ */
private fun drawBlock(canvas: Canvas, x: Int, y: Int, paint: Paint) { private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean) {
val left = boardLeft + x * blockSize val left = boardLeft + x * blockSize
val top = boardTop + y * blockSize val top = boardTop + y * blockSize
val right = left + blockSize val right = left + blockSize
val bottom = top + blockSize val bottom = top + blockSize
// Draw block with a slight inset to create separation // Draw outer glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1) blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(rect, paint) canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
// Draw enhanced glow effect // Draw block
val glowRect = RectF(left, top, right, bottom) blockPaint.apply {
val blockGlowPaint = Paint(glowPaint) color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
if (paint == blockPaint) { alpha = if (isGhost) 30 else 255
val piece = gameBoard.getCurrentPiece()
if (piece != null && isPositionInPiece(x, y, piece)) {
// Set glow color based on piece type
blockGlowPaint.color = getTetrominoColor(piece.type)
blockGlowPaint.alpha = 150
blockGlowPaint.setShadowLayer(3f, 0f, 0f, blockGlowPaint.color)
} }
} canvas.drawRect(left, top, right, bottom, blockPaint)
canvas.drawRect(glowRect, blockGlowPaint)
// 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)
} }
/** /**
@ -524,7 +536,7 @@ class GameView @JvmOverloads constructor(
// Check for double tap (rotate) // Check for double tap (rotate)
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < 250) { if (currentTime - lastTapTime < 200) { // Reduced from 250ms for faster response
// Double tap detected, rotate the piece // Double tap detected, rotate the piece
if (currentTime - lastRotationTime >= rotationCooldown) { if (currentTime - lastRotationTime >= rotationCooldown) {
gameBoard.rotate() gameBoard.rotate()
@ -538,22 +550,33 @@ class GameView @JvmOverloads constructor(
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - lastTouchX val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY val deltaY = event.y - lastTouchY
val currentTime = System.currentTimeMillis()
// Horizontal movement (left/right) // Horizontal movement (left/right) with reduced threshold
if (abs(deltaX) > blockSize) { if (abs(deltaX) > blockSize * 0.5f) { // Reduced from 1.0f for more responsive movement
if (deltaX > 0) { if (deltaX > 0) {
gameBoard.moveRight() gameBoard.moveRight()
} else { } else {
gameBoard.moveLeft() gameBoard.moveLeft()
} }
lastTouchX = event.x lastTouchX = event.x
// Add haptic feedback for movement with cooldown
if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove()
lastMoveTime = currentTime
}
invalidate() invalidate()
} }
// Vertical movement (soft drop) // Vertical movement (soft drop) with reduced threshold
if (deltaY > blockSize / 2) { if (deltaY > blockSize * 0.25f) { // Reduced from 0.5f for more responsive soft drop
gameBoard.moveDown() gameBoard.moveDown()
lastTouchY = event.y lastTouchY = event.y
// Add haptic feedback for movement with cooldown
if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove()
lastMoveTime = currentTime
}
invalidate() invalidate()
} }
} }
@ -565,7 +588,7 @@ class GameView @JvmOverloads constructor(
val deltaX = event.x - startX val deltaX = event.x - startX
// If the movement was fast and downward, treat as hard drop // If the movement was fast and downward, treat as hard drop
if (moveTime > 0 && deltaY > blockSize && (deltaY / moveTime) * 1000 > minSwipeVelocity) { if (moveTime > 0 && deltaY > blockSize * 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) {
gameBoard.hardDrop() gameBoard.hardDrop()
invalidate() invalidate()
} else if (moveTime < minTapTime && } else if (moveTime < minTapTime &&
@ -606,9 +629,11 @@ class GameView @JvmOverloads constructor(
fun isGameOver(): Boolean = gameBoard.isGameOver fun isGameOver(): Boolean = gameBoard.isGameOver
/** /**
* Get the next tetromino * Get the next piece that will be spawned
*/ */
fun getNextPiece() = gameBoard.getNextPiece() fun getNextPiece(): Tetromino? {
return gameBoard.getNextPiece()
}
/** /**
* Clean up resources when view is detached * Clean up resources when view is detached
@ -617,4 +642,31 @@ class GameView @JvmOverloads constructor(
super.onDetachedFromWindow() super.onDetachedFromWindow()
handler.removeCallbacks(gameLoopRunnable) handler.removeCallbacks(gameLoopRunnable)
} }
/**
* Set the game board for this view
*/
fun setGameBoard(board: GameBoard) {
gameBoard = board
invalidate()
}
/**
* Set the haptics handler for this view
*/
fun setHaptics(haptics: GameHaptics) {
gameHaptics = haptics
}
/**
* Resume the game
*/
fun resume() {
if (!isRunning) {
isRunning = true
handler.post(gameLoopRunnable)
}
isPaused = false
invalidate()
}
} }

View file

@ -5,6 +5,7 @@ import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import kotlin.math.min import kotlin.math.min
@ -28,10 +29,10 @@ class NextPieceView @JvmOverloads constructor(
private val glowPaint = Paint().apply { private val glowPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
alpha = 80 alpha = 30
isAntiAlias = true isAntiAlias = true
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 2f strokeWidth = 1.5f
} }
/** /**
@ -60,6 +61,20 @@ class NextPieceView @JvmOverloads constructor(
val previewLeft = (canvas.width - width * previewBlockSize) / 2 val previewLeft = (canvas.width - width * previewBlockSize) / 2
val previewTop = (canvas.height - height * previewBlockSize) / 2 val previewTop = (canvas.height - height * previewBlockSize) / 2
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) { for (y in 0 until height) {
for (x in 0 until width) { for (x in 0 until width) {
if (piece.isBlockAt(x, y)) { if (piece.isBlockAt(x, y)) {
@ -68,9 +83,11 @@ class NextPieceView @JvmOverloads constructor(
val right = left + previewBlockSize val right = left + previewBlockSize
val bottom = top + previewBlockSize val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1) val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
canvas.drawRect(rect, blockPaint) canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom) val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint) canvas.drawRect(glowRect, glowPaint)
} }

View file

@ -48,6 +48,7 @@ class GameBoard(
// Callbacks for game events // Callbacks for game events
var onPieceMove: (() -> Unit)? = null var onPieceMove: (() -> Unit)? = null
var onPieceLock: (() -> Unit)? = null var onPieceLock: (() -> Unit)? = null
var onNextPieceChanged: (() -> Unit)? = null
init { init {
spawnNextPiece() spawnNextPiece()
@ -66,6 +67,7 @@ class GameBoard(
// Take the next piece from the bag // Take the next piece from the bag
nextPiece = Tetromino(bag.removeFirst()) nextPiece = Tetromino(bag.removeFirst())
onNextPieceChanged?.invoke()
} }
/** /**
@ -98,6 +100,11 @@ class GameBoard(
*/ */
fun getHoldPiece(): Tetromino? = holdPiece fun getHoldPiece(): Tetromino? = holdPiece
/**
* Get the next piece that will be spawned
*/
fun getNextPiece(): Tetromino? = nextPiece
/** /**
* Spawns the current tetromino at the top of the board * Spawns the current tetromino at the top of the board
*/ */
@ -475,11 +482,6 @@ class GameBoard(
*/ */
fun getCurrentPiece(): Tetromino? = currentPiece fun getCurrentPiece(): Tetromino? = currentPiece
/**
* Get the next tetromino
*/
fun getNextPiece(): Tetromino? = nextPiece
/** /**
* Check if a cell in the grid is occupied * Check if a cell in the grid is occupied
*/ */
@ -491,6 +493,25 @@ class GameBoard(
} }
} }
/**
* Update the current level and adjust game parameters
*/
fun updateLevel(newLevel: Int) {
level = newLevel.coerceIn(1, 20)
// Update game speed based on level (NES formula)
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
}
/**
* Start a new game
*/
fun startGame() {
reset()
// Initialize pieces
spawnNextPiece()
spawnPiece()
}
/** /**
* Reset the game board * Reset the game board
*/ */
@ -504,10 +525,9 @@ class GameBoard(
// Reset game state // Reset game state
score = 0 score = 0
level = 1
lines = 0 lines = 0
isGameOver = false isGameOver = false
dropInterval = 1000L dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
// Reset scoring state // Reset scoring state
combo = 0 combo = 0
@ -520,9 +540,9 @@ class GameBoard(
canHold = true canHold = true
bag.clear() bag.clear()
// Initialize pieces // Clear current and next pieces
spawnNextPiece() currentPiece = null
spawnPiece() nextPiece = null
} }
/** /**

View file

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

View file

@ -5,112 +5,79 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black" android:background="@color/black"
android:fitsSystemWindows="true"
tools:context=".MainActivity"> tools:context=".MainActivity">
<!-- Game Container with Glow -->
<FrameLayout
android:id="@+id/gameContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.mintris.game.GameView <com.mintris.game.GameView
android:id="@+id/gameView" android:id="@+id/gameView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" <!-- Glowing Border -->
app:layout_constraintRight_toRightOf="parent" <View
app:layout_constraintTop_toTopOf="parent" /> android:id="@+id/glowBorder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/glow_border" />
</FrameLayout>
<!-- HUD Container - Score, Level, Lines --> <!-- HUD Container - Score, Level, Lines -->
<LinearLayout <LinearLayout
android:id="@+id/hudContainer" android:id="@+id/hudContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/scoreLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/score"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold" />
<TextView <TextView
android:id="@+id/scoreText" android:id="@+id/scoreText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="18sp" android:textSize="24sp"
android:textStyle="bold" /> android:fontFamily="sans-serif-light"
tools:text="Score: 0" />
<TextView <TextView
android:id="@+id/levelLabel" android:id="@+id/currentLevelText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/level"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="12sp" android:textSize="24sp"
android:textStyle="bold" /> android:fontFamily="sans-serif-light"
tools:text="Level: 1" />
<TextView
android:id="@+id/levelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/linesLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/lines"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold" />
<TextView <TextView
android:id="@+id/linesText" android:id="@+id/linesText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="18sp" android:textSize="24sp"
android:textStyle="bold" /> android:fontFamily="sans-serif-light"
tools:text="Lines: 0" />
</LinearLayout> </LinearLayout>
<!-- Next Piece Preview --> <!-- Next Piece Preview -->
<LinearLayout
android:id="@+id/nextPieceContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/nextLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold" />
<com.mintris.game.NextPieceView <com.mintris.game.NextPieceView
android:id="@+id/nextPieceView" android:id="@+id/nextPieceView"
android:layout_width="64dp" android:layout_width="80dp"
android:layout_height="64dp" android:layout_height="80dp"
android:layout_marginTop="4dp" android:layout_margin="16dp"
android:background="@color/black" /> app:layout_constraintEnd_toEndOf="parent"
</LinearLayout> app:layout_constraintTop_toBottomOf="@id/hudContainer" />
<!-- Game Over overlay --> <!-- Game Over overlay -->
<LinearLayout <LinearLayout
@ -149,7 +116,7 @@
android:textColor="@color/white" /> android:textColor="@color/white" />
</LinearLayout> </LinearLayout>
<!-- Pause overlay --> <!-- Settings Menu overlay -->
<LinearLayout <LinearLayout
android:id="@+id/pauseContainer" android:id="@+id/pauseContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -159,20 +126,49 @@
android:orientation="vertical" android:orientation="vertical"
android:visibility="gone"> android:visibility="gone">
<Button <TextView
android:id="@+id/resumeButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/settings"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<Button
android:id="@+id/pauseStartButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:text="@string/start"
android:textColor="@color/white"
android:textSize="18sp" />
<Button
android:id="@+id/resumeButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent" android:background="@color/transparent"
android:text="@string/resume" android:text="@string/resume"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="18sp" /> android:textSize="18sp" />
<Button
android:id="@+id/pauseRestartButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/restart"
android:textColor="@color/white"
android:textSize="18sp" />
<LinearLayout <LinearLayout
android:id="@+id/levelSelectorContainer" android:id="@+id/levelSelectorContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="32dp"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:gravity="center">
@ -191,7 +187,7 @@
android:layout_marginTop="8dp"> android:layout_marginTop="8dp">
<Button <Button
android:id="@+id/levelDownButton" android:id="@+id/pauseLevelDownButton"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:background="@color/transparent" android:background="@color/transparent"
@ -200,7 +196,7 @@
android:textSize="24sp" /> android:textSize="24sp" />
<TextView <TextView
android:id="@+id/selectedLevelText" android:id="@+id/pauseLevelText"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:gravity="center" android:gravity="center"
@ -210,7 +206,7 @@
android:textStyle="bold" /> android:textStyle="bold" />
<Button <Button
android:id="@+id/levelUpButton" android:id="@+id/pauseLevelUpButton"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:background="@color/transparent" android:background="@color/transparent"
@ -222,9 +218,9 @@
<Button <Button
android:id="@+id/settingsButton" android:id="@+id/settingsButton"
android:layout_width="wrap_content" android:layout_width="200dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="32dp"
android:background="@color/transparent" android:background="@color/transparent"
android:text="@string/sound_on" android:text="@string/sound_on"
android:textColor="@color/white" android:textColor="@color/white"

View file

@ -1,15 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Mintris</string> <string name="app_name">Mintris</string>
<string name="game_over">GAME OVER</string> <string name="game_over">Game Over</string>
<string name="score">SCORE</string> <string name="score">Score</string>
<string name="level">LEVEL</string> <string name="level">Level</string>
<string name="lines">LINES</string> <string name="lines">Lines</string>
<string name="next">NEXT</string> <string name="next">Next</string>
<string name="play">PLAY</string> <string name="play">Play Again</string>
<string name="resume">RESUME</string> <string name="resume">Resume</string>
<string name="pause">PAUSE</string> <string name="pause">PAUSE</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="sound_on">Sound: ON</string> <string name="start">Start Game</string>
<string name="sound_off">Sound: OFF</string> <string name="restart">Restart</string>
<string name="select_level">Select Level</string> <string name="select_level">Select Level</string>
<string name="sound_on">Sound: On</string>
<string name="sound_off">Sound: Off</string>
</resources> </resources>